Skip to content

feat(equipment): Assembly aggregate (5th equipment aggregate) + rename catchups#26

Closed
xmap wants to merge 5 commits into
hotfix-slice-verb-allowlist-event-reactionfrom
pr-equipment-assembly-aggregate
Closed

feat(equipment): Assembly aggregate (5th equipment aggregate) + rename catchups#26
xmap wants to merge 5 commits into
hotfix-slice-verb-allowlist-event-reactionfrom
pr-equipment-assembly-aggregate

Conversation

@xmap
Copy link
Copy Markdown
Owner

@xmap xmap commented Jun 2, 2026

Summary

Adds the Assembly aggregate (5th equipment aggregate) to the codebase: scaffold + 2 slices (define_assembly, version_assembly). Plus two rename catchups required to land cleanly on origin.

Stacked on #25 (hotfix-slice-verb-allowlist-event-reaction). Merge #25 first, then GitHub auto-rebases this to target main.

Commits

  1. refactor(equipment): rename AssetLevel.ASSEMBLY to COMPONENT (+ catchup for 13+ stale AssetLevel.ASSEMBLY / "Assembly" references in test_asset_events.py + test_register_asset_decider*.py + test_asset_summary_projection.py that landed on origin after the original rename)
  2. feat(equipment): scaffold Assembly aggregate (5th aggregate) (+ folded-in TemplateSlot.required_familiesrequired_family_ids rename to satisfy the Phase 5 UUID-collection-suffix fitness; same drift class as Model.declared_families)
  3. feat(equipment): add define_assembly slice (B.1)
  4. feat(equipment): version_assembly slice (Assembly Sub-Stage B.2)
  5. fix(equipment): complete required_families → required_family_ids rename + register Assembly aggregate in slice-verb fitness (post-cherry-pick: completes the rename across define_assembly + version_assembly slice code, registers assembly in _AGGREGATE_NAMES of the slice-verb fitness, OpenAPI snapshot regen)

Why the extra catchups

  • AssetLevel.ASSEMBLY catchup: the original rename commit landed on a local main that didn't yet have the test files extended by PRs on origin. Porting forward picks up the additional references.
  • required_families rename: TemplateSlot.required_families is a frozenset[UUID]. Phase 5's test_uuid_collection_field_suffix correctly flagged the convention violation (same shape as Model.declared_families fixed earlier). Renamed to required_family_ids across state + events + commands + handlers + tests.
  • assembly in _AGGREGATE_NAMES: a new aggregate's slices were failing test_slice_dir_carries_subject because assembly wasn't in the known subjects set yet.

Test plan

🤖 Generated with Claude Code

xmap and others added 5 commits June 2, 2026 22:56
Frees the "Assembly" token for the incoming Assembly aggregate
(Equipment BC's 5th aggregate, the composition blueprint). Renames
the StrEnum member, rewrites the at-rest CHECK constraint via a
forward-only Atlas migration (drop + UPDATE + add), and aligns the
surface vocabulary across REST routes, MCP tool descriptions,
contract tests, the Asset aggregate and decider docstrings, the
2-BM MCTOptics scenario prose, and the glossary. Adds an
architecture fitness with three checks (symbolic-in-source,
symbolic-in-tests, bare-literal-with-allow-list) so the freed token
cannot silently re-land. UNIT was already taken as the ISA-88 tier
above Assembly, so COMPONENT is the chosen target per the Assembly
aggregate naming lock.

Greenfield posture: no from_stored alias; pre-rename events do not
exist outside local test runs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Equipment BC's 5th aggregate: a content-addressed composition
blueprint that declares required_slots (Family-typed, cardinality-
annotated, optionally pre-Placed) and required_wires (slot-keyed
4-tuples), and exposes a stable presents_as_family_id so
Method.needed_families can treat an instantiated Assembly as one
typed unit at the same level as a single Asset. Fills the gap
operators at APS 2-BM keep hitting: hand-instantiating the same
5-to-12-Asset MCTOptics fixture and re-wiring it from memory.

Scaffold-only: state + 3 main-stream events (Defined / Versioned /
Deprecated) + evolver + content-hash helper + summary projector +
migration + 78 unit tests. AssemblyInstantiated lives on a
separate assembly_instantiation stream and ships with the
instantiate_assembly slice.

Hoists placement_to/from_payload to _placement.py and
drawing_to/from_payload to _drawing.py as the shared codecs;
Mount, Frame, and Assembly events all import from one site,
closing the codec-duplication anti-hook flagged in
project_mount_frame_design Watch items.

canonical_assembly_subset is the single source of truth for the
content-hash subset (name + presents_as_family_id +
required_slots + required_wires + parameter_overrides_schema);
Assembly.content_subset() and compute_assembly_content_hash()
both delegate. Drawing and version are excluded by design;
content_hash is stable across "define" and "version" snapshots
of the same canonical content.

TemplateSlot carries a custom __hash__ that canonicalizes the
dict-valued default_settings via json sort-keys; required because
Assembly.required_slots is frozenset[TemplateSlot].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The first vertical slice on the Assembly aggregate. Genesis FSM
event AssemblyDefined, single-stream append at expected_version=0,
handler-side cross-aggregate Family existence check via load_family
(matches update_asset_settings precedent for FamilyId checks via
event-store replay).

Decider invariants:
  - State must be None -> AssemblyAlreadyExistsError.
  - context.missing_family_ids empty -> FamilyNotFoundForAssemblyError
    carrying the sorted-first missing id for deterministic responses.
  - command.name valid -> InvalidAssemblyNameError via VO.
  - Every wire endpoint references a declared slot ->
    WireReferencesUnknownSlotError (defense-in-depth above the
    Assembly.__post_init__ closure check so a bad command surfaces
    at the API boundary, not as a load-time evolver fault).
  - parameter_overrides_schema valid -> InvalidParameterOverridesSchemaError.

Reconciles InvalidParameterOverridesSchemaError HTTP mapping at 400
via _handle_validation_error (matches InvalidFamilySettingsSchemaError
precedent); state.py docstring updated to match.

Hoists TemplateSlotBody and TemplateWireBody as shared wire-format
mirrors per the rule-of-three precedent (route + tool both consume).
Route body uses list[...] (Pydantic cannot hash BaseModel instances
for frozenset); handler converts to domain frozensets.

Idempotency-Key support via with_idempotency wrap in wire.py.

Tests: 9 decider unit tests, 4 PBT properties, 12 REST contract
tests, 4 MCP contract tests, 1 Postgres integration test. All
14,400 architecture-suite tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Multi-source FSM (Defined | Versioned -> Versioned), replace-on-
version semantic carrying the FULL canonical structural subset
(slots, wires, schema, drawing, version label, presents_as_family_id).
Re-attestation with identical content is allowed and emits a fresh
event, mirroring version_family's deliberate divergence from strict-
not-idempotent.

Decider invariants:
  - State must NOT be None -> AssemblyNotFoundError.
  - state.status in {Defined, Versioned} -> AssemblyCannotVersionError
    when Deprecated (terminal; fork via define_assembly).
  - context.missing_family_ids empty -> FamilyNotFoundForAssemblyError
    (sorted-first missing id, deterministic).
  - command.name valid -> InvalidAssemblyNameError via VO.
  - Every wire endpoint references a declared slot ->
    WireReferencesUnknownSlotError (defense-in-depth above the
    Assembly.__post_init__ closure).
  - parameter_overrides_schema valid -> InvalidParameterOverridesSchemaError.

Handler is longhand because it loads N cross-aggregate Family
streams. Single event-store load for the Assembly stream (folds
state and captures current_version in one call); concurrent Family
loads via asyncio.gather; single-stream append at the captured
expected_version (NOT 0).

AssemblySummaryProjection gains the AssemblyVersioned UPDATE arm:
status=Versioned, replaces name + presents_as_family_id + version +
content_hash wholesale to mirror the aggregate's replace-on-version
semantic.

Tests: 10 decider unit tests, 5 PBT properties, 10 REST contract
tests, 4 MCP contract tests, 1 Postgres integration test. All
14,472 architecture-suite tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ame + register Assembly aggregate in slice-verb fitness

Post-cherry-pick cleanup. The TemplateSlot.required_families field
was renamed to required_family_ids in the scaffold-Assembly commit
(per Phase 5's UUID-collection-suffix convention; the
test_uuid_collection_field_suffix fitness caught it). The rename
applied to aggregate state + events + 5 unit tests in that commit,
but the define_assembly slice (B.1) and version_assembly slice (B.2)
that landed after still referenced the old name in their command,
decider, handler, route, helpers, and contract / MCP tests. This
commit completes the rename across those 12 files plus the
OpenAPI snapshot regen.

Also extends the Phase 5 slice-verb fitness's `_AGGREGATE_NAMES` to
include the new `assembly` aggregate. Without this entry the
`define_assembly` and `version_assembly` slices would fail
test_slice_dir_carries_subject the moment the Assembly aggregate
lands on main.

21674 unit + arch tests + the openapi-drift fitness all pass.
@xmap xmap deleted the branch hotfix-slice-verb-allowlist-event-reaction June 3, 2026 06:08
@xmap xmap closed this Jun 3, 2026
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