Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
871 changes: 871 additions & 0 deletions apps/api/openapi.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions apps/api/src/cora/equipment/_projections.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
FrameChildrenProjection,
FrameConsumersProjection,
FrameSummaryProjection,
ModelSummaryProjection,
MountChildrenProjection,
MountLookupProjection,
MountSummaryProjection,
Expand All @@ -32,6 +33,7 @@ def register_equipment_projections(
registry.register(AssetSummaryProjection())
registry.register(AssetFamilyMembershipProjection())
registry.register(FamilySummaryProjection())
registry.register(ModelSummaryProjection())
registry.register(FrameSummaryProjection())
registry.register(FrameChildrenProjection())
registry.register(FrameConsumersProjection())
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/cora/equipment/aggregates/family/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from cora.equipment.aggregates.family.evolver import evolve, fold
from cora.equipment.aggregates.family.read import (
FamilyLifecycleTimestamps,
list_all_family_ids,
list_asset_ids_in_families,
list_family_ids,
load_family,
Expand Down Expand Up @@ -70,6 +71,7 @@
"evolve",
"fold",
"from_stored",
"list_all_family_ids",
"list_asset_ids_in_families",
"list_family_ids",
"load_family",
Expand Down
61 changes: 59 additions & 2 deletions apps/api/src/cora/equipment/aggregates/family/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,25 @@
resource-API precedent. Mirrors `load_method_timestamps` /
`load_plan_timestamps` / `load_practice_timestamps`.

## Two list_*_family_ids helpers

Two read helpers enumerate Family ids from the summary projection;
they differ ONLY in whether Deprecated Families are filtered out.

`list_family_ids` EXCLUDES Deprecated Families. It backs the
operator-facing discovery path: `inspect_plan_binding`'s candidate
enumeration should not offer a Deprecated Family as a source for
new wiring.

`list_all_family_ids` INCLUDES Deprecated Families. It backs the
cross-BC existence-check path: `define_model` and `add_model_family`
verify that every referenced Family id resolves to a real Family
stream, and per the Model aggregate's design memo Family.deprecation
is an authoring signal, NOT a runtime gate. Binding a Model to a
Deprecated Family is permitted (mirrors the Asset-to-Deprecated-Family
posture); using the discovery filter here would surface a misleading
`FamilyNotFoundError` for a Family that genuinely exists.

`_STREAM_TYPE = "Family"`. The stream-type string is the event store's
internal categorization key for this aggregate.
"""
Expand Down Expand Up @@ -90,17 +109,23 @@ async def load_family_timestamps(
"""


async def list_family_ids(pool: asyncpg.Pool) -> list[UUID]:
async def list_family_ids(pool: asyncpg.Pool | None) -> list[UUID]:
"""Read every non-Deprecated Family id from the summary projection.

Used by `inspect_plan_binding`'s candidate enumeration: callers
Used by `inspect_plan_binding`'s candidate enumeration and by
`define_model`'s cross-BC family_lookup precondition. Callers
iterate every Family, load its aggregate state via `load_family`,
and filter by `Family.affordances` membership. Deprecated
Families are excluded at the SQL layer so they're not offered
as candidate sources (operator can still see Deprecated Families
when they're directly wired into a Plan; this is discovery-side
only).

Returns `[]` when `pool is None` (test / no-database app_env),
mirroring the `load_asset_lifecycle` / `load_asset_location`
null-pool short-circuit. Tests that need a populated lookup
must wire a real pool.

The summary projection doesn't carry an affordances column today
(5j deferred it); when the first caller demands affordance-
filtered queries at scale, ship the column + GIN index here and
Expand All @@ -109,11 +134,43 @@ async def list_family_ids(pool: asyncpg.Pool) -> list[UUID]:
`inspect_plan_binding` crosses 200ms. Pilot scale (~9 Families)
keeps the load-all-then-filter approach cheap.
"""
if pool is None:
return []
async with pool.acquire() as conn:
rows = await conn.fetch(_SELECT_FAMILY_IDS_SQL)
return [row["family_id"] for row in rows]


_SELECT_ALL_FAMILY_IDS_SQL = """
SELECT family_id
FROM proj_equipment_family_summary
ORDER BY family_id::text
"""


async def list_all_family_ids(pool: asyncpg.Pool | None) -> list[UUID]:
"""Read every Family id from the summary projection, INCLUDING Deprecated.

Used by `define_model` and `add_model_family` to verify that every
declared/added Family id resolves to a real Family stream. Per the
Model aggregate's design memo, Family.deprecation is an authoring
signal, NOT a runtime gate: a Model is allowed to declare a
Deprecated Family. Filtering Deprecated rows out here (as
`list_family_ids` does for the discovery path) would surface a
misleading `FamilyNotFoundError` for a Family that genuinely
exists.

Returns `[]` when `pool is None` (test / no-database app_env),
mirroring `list_family_ids`. Tests that need a populated lookup
must wire a real pool.
"""
if pool is None:
return []
async with pool.acquire() as conn:
rows = await conn.fetch(_SELECT_ALL_FAMILY_IDS_SQL)
return [row["family_id"] for row in rows]


_SELECT_ASSET_IDS_BY_FAMILIES_SQL = """
SELECT DISTINCT asset_id
FROM proj_equipment_asset_family_membership
Expand Down
102 changes: 102 additions & 0 deletions apps/api/src/cora/equipment/aggregates/model/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""Model aggregate: state, status enum, errors, events, evolver.

Vertical slices that operate on this aggregate live under
`cora.equipment.features.<verb>_model/` and import from here for
state and event types.
"""

from cora.equipment.aggregates.model.events import (
ModelDefined,
ModelDeprecated,
ModelEvent,
ModelFamilyAdded,
ModelFamilyRemoved,
ModelVersioned,
event_type_name,
from_stored,
to_payload,
)
from cora.equipment.aggregates.model.evolver import evolve, fold
from cora.equipment.aggregates.model.read import list_model_ids, load_model
from cora.equipment.aggregates.model.state import (
MANUFACTURER_IDENTIFIER_MAX_LENGTH,
MANUFACTURER_NAME_MAX_LENGTH,
MODEL_DEPRECATION_REASON_MAX_LENGTH,
MODEL_NAME_MAX_LENGTH,
MODEL_PART_NUMBER_MAX_LENGTH,
MODEL_VERSION_TAG_MAX_LENGTH,
InvalidDeclaredFamiliesError,
InvalidManufacturerIdentifierError,
InvalidManufacturerIdentifierPairingError,
InvalidManufacturerNameError,
InvalidModelDeprecationReasonError,
InvalidModelNameError,
InvalidModelVersionTagError,
InvalidPartNumberError,
Manufacturer,
ManufacturerIdentifier,
ManufacturerIdentifierType,
ManufacturerName,
Model,
ModelAlreadyExistsError,
ModelCannotAddFamilyError,
ModelCannotDeprecateError,
ModelCannotRemoveFamilyError,
ModelCannotVersionError,
ModelDeprecationReason,
ModelFamilyAlreadyPresentError,
ModelFamilyNotPresentError,
ModelName,
ModelNotFoundError,
ModelStatus,
ModelVersionTag,
PartNumber,
)

__all__ = [
"MANUFACTURER_IDENTIFIER_MAX_LENGTH",
"MANUFACTURER_NAME_MAX_LENGTH",
"MODEL_DEPRECATION_REASON_MAX_LENGTH",
"MODEL_NAME_MAX_LENGTH",
"MODEL_PART_NUMBER_MAX_LENGTH",
"MODEL_VERSION_TAG_MAX_LENGTH",
"InvalidDeclaredFamiliesError",
"InvalidManufacturerIdentifierError",
"InvalidManufacturerIdentifierPairingError",
"InvalidManufacturerNameError",
"InvalidModelDeprecationReasonError",
"InvalidModelNameError",
"InvalidModelVersionTagError",
"InvalidPartNumberError",
"Manufacturer",
"ManufacturerIdentifier",
"ManufacturerIdentifierType",
"ManufacturerName",
"Model",
"ModelAlreadyExistsError",
"ModelCannotAddFamilyError",
"ModelCannotDeprecateError",
"ModelCannotRemoveFamilyError",
"ModelCannotVersionError",
"ModelDefined",
"ModelDeprecated",
"ModelDeprecationReason",
"ModelEvent",
"ModelFamilyAdded",
"ModelFamilyAlreadyPresentError",
"ModelFamilyNotPresentError",
"ModelFamilyRemoved",
"ModelName",
"ModelNotFoundError",
"ModelStatus",
"ModelVersionTag",
"ModelVersioned",
"PartNumber",
"event_type_name",
"evolve",
"fold",
"from_stored",
"list_model_ids",
"load_model",
"to_payload",
]
Loading
Loading