Skip to content

Prove the Diffractometer Assembly materializes + exercises the affordance-union gate#538

Merged
xmap merged 1 commit into
mainfrom
worktree-diffractometer-fixture
Jul 3, 2026
Merged

Prove the Diffractometer Assembly materializes + exercises the affordance-union gate#538
xmap merged 1 commit into
mainfrom
worktree-diffractometer-fixture

Conversation

@xmap

@xmap xmap commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Materializes and proves the Diffractometer Assembly end-to-end — the composed multi-circle scattering instrument that presents the Positioner Role by composing a Goniometer + zero-or-more detector-arm RotaryStage + a reciprocal-space PseudoAxis.

It is fleet-attested (4-ID, 8-ID, ~9 more) and defined in the catalog, but had never been materialized as a Fixture — so the register_fixture affordance-union check (Watch Item 10, shipped in #534) on a presents_as Assembly had never actually run on a composed instrument. The existing Microscope fixture tests use an Assembly with empty presents_as, which skips the gate.

This is that first exercise, over the REST surface, using the real seeded Positioner Role + constituent Family ids:

  • Happy path: Goniometer (covers Homeable+Limitable) + detector-arm RotaryStage + reciprocal-space PseudoAxis → constituent affordance union covers Positioner's required set → fixture registers (201).
  • Negative: Goniometer stripped of Homeable/Limitable AND no detector-arm RotaryStage bound (detector_arm is ZeroOrMore) → no constituent supplies the required set → 409 FixtureCannotPresentRoleError.

Gate-semantics finding (recorded in the test)

The negative case surfaced that the affordance union is taken across all bound constituents — so a presents_as Assembly is satisfied when any bound part supplies the required affordances. This is the intended Watch-Item-10 v1 semantics (union coverage: a coarse "the instrument contains the capability" check, not "this specific slot provides it"). Documented as a watch-item, not changed.

SPEC-1 assessed (no change)

The EmissionSpectrometer / SpectrometerArm catalog notes correctly model those as single Families (presenting Detector / Positioner) and defer only the narrow child-Asset-per-crystal question. They do not claim to be CORA Assemblies, so no note change is warranted.

Test plan

  • pytest tests/contract -k "fixture or assembl or diffractometer" (117 passed, incl. the 2 new)
  • ruff + pyright clean on the new test
  • test-only change (no src/catalog edit); blast radius contained

Generated with Claude Code

…rcises the affordance-union gate

The Diffractometer is the catalog Assembly that presents the Positioner
Role by composing a Goniometer + zero-or-more detector-arm RotaryStage +
a reciprocal-space PseudoAxis. It is fleet-attested (4-ID, 8-ID, ~9 more)
but had never been materialized as a Fixture -- so the register_fixture
affordance-union check (Watch Item 10) on a presents_as Assembly had never
actually run on a composed instrument (the existing Microscope fixture
tests use an Assembly with empty presents_as, which SKIPS the gate).

This is that first exercise, end-to-end over the REST surface using the
real seeded Positioner Role + constituent Family ids:

  - happy path: Goniometer (covers Homeable+Limitable) + detector-arm
    RotaryStage + reciprocal-space PseudoAxis -> the constituent affordance
    union covers Positioner's required set -> fixture registers (201).
  - negative: Goniometer stripped of Homeable/Limitable AND no detector-arm
    RotaryStage bound (detector_arm is ZeroOrMore) -> no constituent
    supplies the required set -> 409 (FixtureCannotPresentRoleError).

Gate-semantics note recorded in the test: the affordance union is taken
across ALL bound constituents, so a presents_as Assembly is satisfied when
ANY bound part supplies the required affordances (union coverage, the
intended Watch-Item-10 v1 semantics -- a coarse "the instrument contains
the capability" check, not "this specific slot provides it").

SPEC-1 assessed alongside: the EmissionSpectrometer / SpectrometerArm
notes correctly model those as single Families presenting Detector /
Positioner and defer only the narrow child-Asset-per-crystal question --
they do NOT claim to be CORA Assemblies, so no note change is warranted.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

Coverage report

This PR does not seem to contain any modification to coverable code.

xmap added a commit that referenced this pull request Jul 3, 2026
…g at runtime

The #539 catalog conversions introduced a "mixed" method shape (a Detector
role_kind slot PLUS a needed_family coverage requirement for a no-Role
family: single_crystal_diffraction / surface_diffraction /
grazing_incidence_scattering keep PseudoAxis, transmission_xray_microscopy
keeps ZonePlate). That coexistence was asserted from the schema (both
fields, no XOR) but never exercised at runtime -- the #539 commit was
catalog-only, and catalog methods are docs-only, so no binding fired.

This adds the missing proof over the REST surface:
  - happy: a Camera (Detector) + a PseudoAxis asset -> define_plan accepts
    (needed_family coverage holds) AND the Detector role_kind slot binds.
  - negative: a Plan with only the Camera (no PseudoAxis) -> define_plan
    409 (needed_family_ids not covered), even though the Detector slot
    could bind -- proving the role slot and the family-coverage set are
    independent and both enforced.

Closes the coverage gap between #539 (catalog) and the runtime binding
machinery, matching the proof discipline used for tomography (#534),
xpcs/Shutter (#534), and the Diffractometer fixture (#538).

Co-Authored-By: Claude <noreply@anthropic.com>
@xmap xmap merged commit e11d478 into main Jul 3, 2026
16 checks passed
@xmap xmap deleted the worktree-diffractometer-fixture branch July 3, 2026 13:28
xmap added a commit that referenced this pull request Jul 3, 2026
* feat(catalog): bind the new fleet technique-methods by Role

Extends the role_kind binding to the ~20 technique-methods that landed on
main from the fleet-expansion work, applying the same per-method
abstraction assessment used for the original method set.

Full role-mappable (14) -> required_roles, every family presents a Role:
  powder_diffraction, small_wide_angle_scattering            -> [Detector]
  radiography, xray_emission_spectroscopy                    -> [Detector]
    (radiography: Scintillator drops -- detector-Assembly constituent)
  macromolecular_crystallography                             -> [Positioner, Detector]
  coherent_diffraction_imaging, ptychography                 -> [Detector, Positioner]
  xray_photoelectron_spectroscopy, hard_xray_photoelectron_spectroscopy,
    angle_resolved_photoemission                             -> [Detector, Positioner]
  serial_crystallography                          -> [Positioner, Detector, Controller]
  xray_fluorescence_mapping                                  -> [Sensor, Positioner]
  resonant_inelastic_scattering, inelastic_scattering        -> [Positioner]
    (SpectrometerArm presents Positioner)

Mixed (4) -> Detector role + the no-Role family kept anatomical:
  single_crystal_diffraction, surface_diffraction,
  grazing_incidence_scattering    -> roles [Detector] + needed [PseudoAxis]
  transmission_xray_microscopy    -> roles [Detector] + needed [ZonePlate]
  The reciprocal-space PseudoAxis and the ZonePlate objective present no
  Role (computed coordinate / imaging optic, not functional contracts), so
  they stay on needed_families; each carries a note explaining the mix.

Kept family_id (assessed, unchanged): absorption_spectroscopy +
continuous_absorption_spectroscopy (Monochromator only, no Role),
energy_characterization, beamline_energy_change (the deferred energy
abstraction), hexapod_reboot (device-specific recovery).

A Method may carry both required_roles and needed_families (independent
fields, no XOR) -- that is what the mixed methods use. All Role targets
verified against catalog presents_as (EmissionSpectrometer->Detector,
SpectrometerArm->Positioner, EnergyDispersiveSpectrometer->Sensor, etc.).

Co-Authored-By: Claude <noreply@anthropic.com>

* test(recipe): prove the mixed role_kind + needed_family method binding at runtime

The #539 catalog conversions introduced a "mixed" method shape (a Detector
role_kind slot PLUS a needed_family coverage requirement for a no-Role
family: single_crystal_diffraction / surface_diffraction /
grazing_incidence_scattering keep PseudoAxis, transmission_xray_microscopy
keeps ZonePlate). That coexistence was asserted from the schema (both
fields, no XOR) but never exercised at runtime -- the #539 commit was
catalog-only, and catalog methods are docs-only, so no binding fired.

This adds the missing proof over the REST surface:
  - happy: a Camera (Detector) + a PseudoAxis asset -> define_plan accepts
    (needed_family coverage holds) AND the Detector role_kind slot binds.
  - negative: a Plan with only the Camera (no PseudoAxis) -> define_plan
    409 (needed_family_ids not covered), even though the Detector slot
    could bind -- proving the role slot and the family-coverage set are
    independent and both enforced.

Closes the coverage gap between #539 (catalog) and the runtime binding
machinery, matching the proof discipline used for tomography (#534),
xpcs/Shutter (#534), and the Diffractometer fixture (#538).

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant