feat(equipment): Assembly aggregate (5th equipment aggregate) + rename catchups#28
Merged
Conversation
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.
Coverage reportClick to see where and how coverage changed
The report is truncated to 25 files out of 33. To see the full report, please visit the workflow summary page. This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the Assembly aggregate (5th equipment aggregate): scaffold + 2 slices (
define_assembly,version_assembly). Plus two rename catchups required to land cleanly on origin/main.(Re-opens PR #26 which was auto-closed when its stacked base #25 merged.)
Commits
refactor(equipment): rename AssetLevel.ASSEMBLY to COMPONENT(+ catchup for 13+ stale references in test_asset_events.py, test_register_asset_decider*.py, test_asset_summary_projection.py)feat(equipment): scaffold Assembly aggregate (5th aggregate)(+ folded-inTemplateSlot.required_families→required_family_idsrename to satisfy the Phase 5 UUID-collection-suffix fitness)feat(equipment): add define_assembly slice (B.1)feat(equipment): version_assembly slice (Assembly Sub-Stage B.2)fix(equipment): complete required_families → required_family_ids rename + register Assembly aggregate in slice-verb fitnessWhy the extra catchups
test_uuid_collection_field_suffixcorrectly flagged the convention violation (same shape asModel.declared_familiesfixed earlier).test_slice_dir_carries_subject.Test plan
🤖 Generated with Claude Code