Skip to content

feat(equipment): register_fixture rejects Decommissioned bound assets#40

Merged
xmap merged 1 commit into
mainfrom
register-fixture-rejects-decommissioned
Jun 5, 2026
Merged

feat(equipment): register_fixture rejects Decommissioned bound assets#40
xmap merged 1 commit into
mainfrom
register-fixture-rejects-decommissioned

Conversation

@xmap
Copy link
Copy Markdown
Owner

@xmap xmap commented Jun 5, 2026

Summary

Adds a single cross-aggregate guard at register_fixture time: every Asset referenced by slot_asset_bindings must NOT be Decommissioned. Mirrors the sibling AssetCannotAttachToFixtureError precondition at attach-time and rejects at register-time so the operator doesn't register a Fixture that would inevitably fail later at attach_asset_to_fixture (Fixture is single-event-genesis and cannot be amended).

  • New FixtureAssetNotAttachableError carries the sorted-first offending asset_id and current lifecycle string, mirroring the FixtureAssetNotFoundError deterministic-error precedent in the same decider.
  • RegisterFixtureContext gains a lifecycle_by_asset_id dict populated from the existing per-Asset load_asset gather (no extra round-trip, no new projection). Default empty dict means decider-only unit tests that exercise other invariants leave the guard inactive (mirrors family_ids_by_asset_id's relaxed default).
  • Decider guard ordering: existence (FixtureAssetNotFoundError) → NEW lifecycle (FixtureAssetNotAttachableError) → unknown-slot → cardinality → family-mismatch → param-overrides. Operator sees the most actionable error first.
  • Route + OpenAPI 409 description list the new cause; routes.py wires the new error into the existing _handle_cannot_transition family alongside its Asset-BC mirror.

Zero existing tests break because every existing happy-path test uses Active assets.

Closes INV-5 from the Fixture+Mount+Asset alignment plan; first half of Slice 3 (INV-4 orphan-binding guard deferred to Slice 3b to keep this slice ripple-free). Slice 3a in the Option A sequence.

Test plan

  • Two new decider unit tests (guard fires with sorted-first determinism + empty-dict short-circuit) — green locally
  • One integration test exercising the full handler stack end-to-end against Postgres (register_asset → decommission_asset → register_fixture → FixtureAssetNotAttachableError) — green locally
  • Existing register_fixture decider + endpoint suite (19 tests) — green locally
  • CI confirms full suite + architecture fitnesses pass

🤖 Generated with Claude Code

Adds a single cross-aggregate guard: every Asset referenced by
`slot_asset_bindings` must NOT be Decommissioned. Mirrors the
sibling AssetCannotAttachToFixtureError precondition at attach-time
and rejects at register-time so the operator does not register a
Fixture that would inevitably fail later at attach_asset_to_fixture
(Fixture is single-event-genesis and cannot be amended).

- New FixtureAssetNotAttachableError carries the sorted-first
  offending asset_id and current lifecycle string, mirroring the
  FixtureAssetNotFoundError deterministic-error precedent in the
  same decider.
- RegisterFixtureContext gains a `lifecycle_by_asset_id` dict
  populated from the existing per-Asset load_asset gather (no extra
  round-trip, no new projection). Default empty dict means
  decider-only unit tests that exercise other invariants leave the
  guard inactive (mirrors family_ids_by_asset_id's relaxed default).
- Decider guard ordering: existence (FixtureAssetNotFoundError)
  -> NEW lifecycle (FixtureAssetNotAttachableError) -> unknown-slot
  -> cardinality -> family-mismatch -> param-overrides. Operator
  sees the most actionable error first.
- Route + OpenAPI 409 description list the new cause; routes.py
  wires the new error into the existing _handle_cannot_transition
  family alongside its Asset-BC mirror.

Zero existing tests break because every existing happy-path test
uses Active assets. Two new decider unit tests (guard fires with
sorted-first determinism + empty-dict short-circuit), plus one
integration test exercising the full handler stack end-to-end
against Postgres (register_asset -> decommission_asset ->
register_fixture -> FixtureAssetNotAttachableError).

Closes INV-5 from the Fixture+Mount+Asset alignment plan; first
half of Slice 3 (INV-4 orphan-binding guard deferred to Slice 3b
to keep this slice ripple-free). Slice 3a in the Option A sequence.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 5, 2026

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  apps/api/src/cora/equipment
  routes.py
  apps/api/src/cora/equipment/aggregates/assembly
  state.py
  apps/api/src/cora/equipment/features/register_fixture
  context.py
  decider.py
  handler.py
Project Total  

This report was generated by python-coverage-comment-action

@xmap xmap merged commit cefdefc into main Jun 5, 2026
4 checks passed
@xmap xmap deleted the register-fixture-rejects-decommissioned branch June 5, 2026 08:30
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