Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3ed81cd
Validate measurement subjects at compile and expose declared selectors
trissim Sep 29, 2026
e56b977
Verify measurement declaration journeys and adapter output ownership
trissim Sep 29, 2026
688e4e7
Record owned synthetic validation scratch cleanup
trissim Sep 29, 2026
9f5d444
Merge remote-tracking branch 'openhcsdev/main' into feat/exact-label-…
trissim Sep 29, 2026
b64d9ae
Resolve declared secondary artifact inputs by typed source context
trissim Sep 29, 2026
ae29c26
Record current-main source build and strengthen missing-source regres…
trissim Sep 29, 2026
d7515e7
Preserve exact artifact admission for unscoped and partial contexts
trissim Sep 29, 2026
dd02a00
Trace declared paired input context in the continuous selector journey
trissim Sep 29, 2026
8b71523
Merge remote-tracking branch 'openhcsdev/main' into feat/exact-label-…
trissim Sep 29, 2026
a38be98
Validate all output kinds through declared recording policies
trissim Sep 29, 2026
21c15ad
Merge remote-tracking branch 'openhcsdev/main' into feat/exact-label-…
trissim Sep 29, 2026
153f643
Retain exact label-owned measurement group-scope regression
trissim Sep 29, 2026
684e4bb
Merge remote-tracking branch 'openhcsdev/main' into feat/exact-label-…
trissim Sep 30, 2026
94595f2
Strengthen subject journey with typed feature owner and source lineage
trissim Sep 30, 2026
85d8b2a
Merge remote-tracking branch 'openhcsdev/main' into feat/exact-label-…
trissim Sep 30, 2026
3dfa616
Bind measurement journey lineage through existing main-flow output owner
trissim Sep 30, 2026
4f43323
Validate finalized input ABI before dependent output obligations
trissim Sep 30, 2026
b7e6a2f
Merge remote-tracking branch 'openhcsdev/main' into feat/exact-label-…
trissim Sep 30, 2026
9271a55
Record terminal measurement journey and current main integration
trissim Sep 30, 2026
5296400
Keep artifact policy and input context projection on declaration owners
trissim Sep 30, 2026
20bf7e4
Use nominal postponed feature-owner return annotation
trissim Sep 30, 2026
04d1be4
Record current-main journey and packaged ratchet owner cutover
trissim Sep 30, 2026
0e04ad6
Merge remote-tracking branch 'openhcsdev/main' into feat/exact-label-…
trissim Sep 30, 2026
9437c44
Record current main journey and explicit owner coordination
trissim Sep 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
707 changes: 707 additions & 0 deletions docs/plans/issue133_204_measurement_contract_checkpoint_20260929.md

Large diffs are not rendered by default.

104 changes: 104 additions & 0 deletions docs/plans/issue204_catalog_diagnosis_20260929.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
## Clarified diagnosis — 2026-09-29

The exact selector already exists on the CellProfiler declaration. This is a
catalog discoverability defect, not a missing selector mechanism:

- `MeasureObjectIntensityModule.object_measurement_binding.parameter_name` is
`None`, but `require_parameter_name()` supplies
`select_object_sets_to_measure`; the binding is repeated.
- Author `select_object_sets_to_measure=("Cells",)` in the measurement
`FunctionStep` kwargs, where `Cells` is the exact declared secondary output.
This compile-time selector is consumed by declaration reconstruction.
- `labels` is the runtime payload parameter, not an authorable selector.
- The agent catalog projected the raw optional `parameter_name`, concealing the
existing derived selector. Draft [PR205](https://github.com/OpenHCSDev/openhcs/pull/205)
projects `require_parameter_name()` and explains exact tuple authoring.

Current source verification uses the real `PipelineDocumentAuthority` and
`FunctionStepDocumentAuthority` render/parse routes followed by
`InProcessCompileInspectionGateway.compile`. With primary `Nuclei` and
secondary `Cells` producers, `("Cells",)` survives both roundtrips and the
compiled scalar `labels` edge points only to secondary source step 1. Omitting
selection still fails closed; a one-producer pipeline still compiles.

A complete same-source two-producer run additionally proves that the real
orchestrator measures the grown secondary `Cells` region, not primary `Nuclei`.
The current focused suite passed 80 unit cases and five real headless journey
cases; the paired-channel regression below was explicitly deselected, not fixed.

The paired DNA-channel-1 / actin-channel-2 runtime reproduction exposes a separate
input-scope defect before measurement: the secondary step requests exact
`Nuclei` labels under channel 2's fixed execution scope, while their record
belongs to channel 1. `RuntimeArtifactInput._matches_execution_scope` rejects
that address-matched record. This remains an explicit acceptance blocker; it is
not evidence of a missing selector and is not bypassed with fallback selection.
Zeno now owns that runtime/input change under the explicit scope extension. PR205
reuses the existing secondary input's `InputGroupLineageSourceRelation` and
declaration-derived typed image-set identity policy, not a global channel-ignore
flag or another selector. All 48 lightweight runtime-store regressions pass,
including both adapter consumers and wrong well/site/z/time/producer, missing
relation/source, wrong referenced source, absent site plane, and ambiguity cases.
The full paired headless journey could not acquire Euler's validation lock
(exit 75, no pytest started) and remains pending without deselection. Actual GUI
interaction and installed-stack verification also remain coordinator-owned.

The coordinator's empty/asymmetric scope witness is now covered explicitly.
It reproduced four address-matched input failures when declaration projection
left no shared context keys. The artifact boundary now admits vacuous additional
constraints after exact producer selection; the unrelated plane-identity
compatibility predicate remains unchanged and still rejects empty identities.
All 56 runtime-store cases pass after this correction. Full paired execution
awaits the explicit worker validation-slot handoff after the parent's #151/#212
MCP/installation checkpoint; no first-release race or baseline change will occur.

## Original observed failure and acceptance (preserved)

Original title: Allow typed exact object-label selection for scalar CellProfiler measurement steps

## Problem

An OpenHCS pipeline that produces both primary and secondary object-label
artifacts cannot add a scalar `measure_object_intensity` step to measure the
secondary cells on the actin channel. Compile inspection rejects the step
before execution:

```
Callable 'measure_object_intensity' scalar artifact-fed parameter 'labels' has multiple exact artifact occurrences (IdentifyPrimaryObjects_1_object_labels_1, IdentifySecondaryObjects_2_object_labels_1).
```

The ambiguity is correctly rejected, but the public PipelineDocument / UI
code-document route must expose a typed way to select the intended exact
producer for that scalar `labels` input. Supplying `labels` as a function kwarg
is not valid because it is runtime-owned; the UI code-document roundtrip also
removed such a kwarg in the observed H003 diagnostic. This is not a request
to guess the newest artifact or silently pick one.

## Minimal reproducer

1. In one complete PipelineDocument, step 0 calls
`identify_primary_objects` on a DNA plane and emits primary labels.
2. Step 1 calls `identify_secondary_objects` on a paired actin plane and
consumes those primary labels, emitting secondary labels.
3. Step 2 calls `measure_object_intensity` on the actin plane to measure the
secondary cells.
4. Compile or call `openhcs_inspect_pipeline_source_artifact_plan`.

The current result is the multiple-exact-occurrences error above. The two
source planes may be tiny synthetic arrays; no biological input is needed to
reproduce this declaration boundary.

## Acceptance

- A declaration-owned typed selector identifies the exact secondary-label
producer for the scalar `labels` parameter, through the headless and UI
code-document routes; roundtrip preserves it.
- Compile plan shows one exact secondary-label artifact input for the
measurement step, and a focused synthetic run measures the secondary
objects rather than primary objects.
- An omitted selector when both are available still fails closed with a
useful ambiguity diagnostic; existing unambiguous pipelines still compile.
- Tests cover compile, code-document roundtrip, and runtime binding without
parallel registries, string-name heuristics, or silent fallback.

Owner: OpenHCS blind-analysis coordinator; continue in an isolated worktree,
keeping the current H003 blind worker independent of prior trial findings.
85 changes: 85 additions & 0 deletions docs/plans/measurement_group_scope_triage_20260929.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Measurement group-scope compile incident (#204 / PR205)

Owner: Zeno. Classification: invalid authored grouping, not a missing backend
scope projection. This technical triage adds regression evidence to PR205; it
does not close #204's separate paired-channel runtime/live acceptance.

## Actual request provenance

- Frozen installed harness: `main` at `283b21275`, unchanged by this worker.
- Server 5793, job `4f8e1d55-07f0-4bde-9f37-d24c0ea398f9`, terminal failure
2026-09-29 20:24:35 UTC (16:24:35 in the local log).
- Authorized log:
`/home/ts/wt/openhcs-h003-uncoached-20260929/output/runtime/xdg-data/openhcs/logs/openhcs_zmq_server_port_5793_1790712796745242.log`.
Request line 799 records `compile_only=True`, seven steps,
`pipeline_sha=5fb62ebed47d`, `request_sig=48d0e5ebf4c4`; lines 845–846
record the terminal compile error, before measurement execution.
- Original technical request source:
`/home/ts/wt/openhcs-h003-uncoached-20260929/output/trial03/PipelineDocument.py`.
Its SHA256 is
`5fb62ebed47d9ed0f11f4499c26a96ac7107e655f47df64c7a51124cdf03f88d`,
matching the original server request. AST inspection extracted only declaration
identities and processing scopes, not scientific settings or outputs.
- Global scope declares `GroupBy.CHANNEL` and variable `SITE`. `Cells` is
explicitly produced under group `"2"`; `MeasureCells` explicitly dispatches
`measure_object_size_shape` under group `"1"`, selecting exact `"Cells"`
through the existing `select_object_sets_to_measure` binding.
- The separately saved `trial04/rejected-grouping.PipelineDocument.py` has hash
`09c3b345c49f...`, matching later job
`c5005087-f638-4a85-a8fb-abe4149e9df6`, not the original request. Its technical
compile receipt preserves the same error. It is corroboration, not a substitute
for the original request identity.

## Owner and projection evidence

`ObjectLabelDrivenPrimaryImageInputPolicy.invocation_domain_inputs` makes the
exact selected labels input the size/shape invocation domain.
`MeasurementArtifactOutputModule.measurement_output_relations` already emits
`GroupLineageSourceRelation(Cells input ref)`; `ObjectMeasurementInputModule`
also emits the object measurement subject. No declaration field is missing.

`ArtifactProducer.has_explicit_invocation_group_ownership` preserves authored
grouped dispatch. `PathPlannerArtifactStage.output_groups_from_declared_relations`
therefore preserves output group `"1"`. The exact labels source has compiled
group `"2"`, so `output_group_scope_sources_by_group` correctly rejects it with:

```text
MeasureCells_4_measurements ... group '1' has no declared group-scope source.
```

The relevant planner, producer, module-contract, module-relation and primary-image
policy files are unchanged from frozen `283b21275`. No backend scope relaxation,
new selector, registry, adapter exemption, or global channel exception is needed.
The separate #204 secondary-input runtime repair remains intact.

## Focused regression and acceptance boundary

`test_real_object_measurement_preserves_selected_labels_group_scope` reconstructs
the real `MeasureObjectSizeShapeModule` public invocation, canonical module
numbering and declaration contract, then runs actual artifact graph extraction,
planner map compilation and `compile_function_pattern`. The tiny synthetic
producer uses the exact `Cells` identity and channel group `"2"`:

- Explicit measurement group `"1"` reproduces the original terminal error.
- Changing only that authored group to `"2"` compiles, preserving the exact label
input, object subject and output-to-label group-scope mapping.
- A non-dict invocation derives group `"2"` from the declaration-owned labels
domain. Its exact selector is consumed, not forwarded as a runtime kwarg.

Three cases passed (99 deselected), 1.99 seconds. Combined with the output-policy
review regressions, **23 passed**, 1.87 seconds, after normal main integration
`21c15ad41` over `de23449a4`. All nine OpenHCS/dependency imports resolved into
this worktree using the existing shared venv and recorded submodule src paths.
The resource guard reported 10.2 GiB available RAM and a historical-swap warning;
only lightweight provider-free checks proceeded. No replay, MCP/JVM/GUI startup,
installation change, held-out/reference inspection or author message occurred.

NRA/refactor-audit coverage is focused, not a complete semantic/proof scan:
IDEN-1/7 keeps authored dispatch, selected producer and measurement subject
distinct; BOUND-2 uses the existing typed owners; MEMB-1/2 introduces no roster;
IMPL-4/10 preserves declaration-driven compile rejection before execution.
The three new cases require **zero production edits**. Tests and this receipt
are the only new scope-specific changes. Actual installed paired-channel and GUI
acceptance remains pending the coordinator's serialized validation handoff after
Confucius releases the frozen runtime. No new issue is needed for this authored
contract error; the case is retained on existing #204 / PR205.
2 changes: 1 addition & 1 deletion openhcs/agent/dto/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ class CellProfilerArtifactBindingSummary:
direction: str
kind: str
setting_names: tuple[str, ...]
parameter_name: str | None = None
parameter_name: str
runtime_parameter_name: str | None = None
repeated: bool = False

Expand Down
4 changes: 2 additions & 2 deletions openhcs/agent/services/function_catalog_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -1145,7 +1145,7 @@ def _cellprofiler_artifact_binding_summary(
direction="input" if plan_type is ArtifactInputPlan else "output",
kind=binding.require_artifact_type().require_value(),
setting_names=setting_names(binding.setting_name),
parameter_name=binding.parameter_name,
parameter_name=binding.require_parameter_name(),
runtime_parameter_name=binding.runtime_parameter_name,
repeated=binding.repeated,
)
Expand All @@ -1162,7 +1162,7 @@ def _source_binding_rule(
contract: CallableContract,
) -> str | None:
if cellprofiler_module is not None:
return "CellProfiler exact artifact names are resolved from the module declaration, concrete FunctionStep groups, and compile-time setting identities. Callable-level artifact arrays can therefore be empty before compilation; inspect the module artifact_bindings here and the compiled artifact plan for exact names."
return "CellProfiler exact artifact names are resolved from the module declaration, concrete FunctionStep groups, and compile-time setting identities. Callable-level artifact arrays can therefore be empty before compilation; inspect the module artifact_bindings here and the compiled artifact plan for exact names. Author selectors in FunctionStep func kwargs using each binding's parameter_name; for repeated bindings, use a tuple of exact artifact names (a one-element tuple selects one producer). These compile-time selectors are consumed by the declaration, not passed to the callable. runtime_parameter_name is runtime-owned: do not pass labels or other runtime payloads as function kwargs. When multiple label producers are available, select the exact intended output name; omission remains ambiguous and fails closed."
if contract.artifact_inputs:
return "Artifact input bindings are resolved from canonical CallableContract artifact_inputs during compilation."
return None
Expand Down
60 changes: 59 additions & 1 deletion openhcs/core/artifact_key_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,65 @@

from abc import ABC, abstractmethod
from collections.abc import Mapping
from typing import TypeVar
from typing import ClassVar, TypeVar

from openhcs.core.artifacts import (
ArtifactOutputPlan,
ArtifactPlan,
ArtifactSpec,
ArtifactSpecCollection,
ArtifactSpecRef,
MeasurementsArtifactType,
)

ArtifactPlanT = TypeVar("ArtifactPlanT", bound=ArtifactPlan)


class ArtifactOutputPolicy(ABC):
"""Declaration-owned recording and payload obligations for output artifacts."""

records_outputs: ClassVar[bool]

@classmethod
def validate_output_declarations(cls, specs: ArtifactSpecCollection) -> None:
"""Run common kind invariants before the ownership-specific obligation."""

for spec in specs.for_plan_type(ArtifactOutputPlan):
spec.artifact_type.validate_output_declaration(spec)
cls.validate_payload_declaration(spec)

@classmethod
@abstractmethod
def validate_payload_declaration(cls, spec: ArtifactSpec) -> None:
"""Validate what this owner needs to turn the declaration into a payload."""


class NativeReturnArtifactOutputPolicy(ArtifactOutputPolicy):
"""Returned payloads are normalized and recorded by the native runtime."""

records_outputs = False

@classmethod
def validate_payload_declaration(cls, spec: ArtifactSpec) -> None:
spec.artifact_type.validate_native_output_declaration(spec)


class AdapterRecordedArtifactOutputPolicy(ArtifactOutputPolicy):
"""The declared adapter records payloads without bypassing kind invariants."""

records_outputs = True

@classmethod
def validate_payload_declaration(cls, spec: ArtifactSpec) -> None:
spec.artifact_type.validate_recorded_output_declaration(spec, cls)

@classmethod
def validate_measurement_subject(cls, spec: ArtifactSpec) -> None:
"""Ordinary recorded tables have one declaration-owned subject."""

MeasurementsArtifactType.require_output_subject(spec)


class ArtifactPlanKeySelector(ABC):
"""Nominal interface for declarations that select compiled artifact plans."""

Expand Down Expand Up @@ -55,6 +102,17 @@ def select_plans(
selected.append(plan)
return tuple(selected)

@property
def artifact_output_policy(self) -> type[ArtifactOutputPolicy]:
"""Return the native output owner unless this declaration supplies another."""

return NativeReturnArtifactOutputPolicy

def validate_artifact_output_declarations(self) -> None:
"""Validate every output kind through its declared payload owner."""

self.artifact_output_policy.validate_output_declarations(self.artifact_specs)

def validate_artifact_relation_refs(self, *, owner_name: str) -> None:
self.artifact_specs.validate_registered_relation_refs(
owner_name=owner_name,
Expand Down
Loading
Loading