Trackable Assets Domain
The Trackable Assets domain (trackable_asset_v1) is a bounded context responsible for managing the complete lifecycle of physical assets that can be tracked, located, and attributed within an estate’s structural hierarchy. This domain handles asset creation, status management, positioning (both geographic and structural), commenting, imagery, and custom field management.
Overview
Section titled “Overview”The Trackable Assets domain sits at the intersection of physical asset management and structural placement. It manages assets as they move through their lifecycle—from creation in draft status through active operation, maintenance, decommissioning, and deletion. Each asset can be positioned in multiple ways:
- Structural Assignment: Placement within the estate’s topology (site → building → room → level)
- Geographic Position: GeoJSON-based coordinates with optional altitude and accuracy metadata
- Layer Association: Optional placement within floor plan layers
Key Aggregates and Responsibilities
Section titled “Key Aggregates and Responsibilities”TrackableAssetAggregate
Section titled “TrackableAssetAggregate”The root aggregate of this domain, representing a single trackable asset with complete lifecycle state.
Identity:
TrackableAssetId: Unique identifier for the asset
Core Attributes:
final AssetNickname name; // User-friendly namefinal AssetDescription? description; // Optional descriptionfinal AssetType assetType; // Catalog type code (e.g., 'hvac_ahu', 'fire_extinguisher')final AssetStatus status; // Current operational status (draft, active, inactive, maintenance, decommissioned, deleted)Positioning:
final GeoPosition? geoPosition; // Geographic position (lat/lng, altitude, accuracy)final StructuralAssignment? structuralAssignment; // Placement in estate structurefinal LayerId? layerId; // Optional layer association within a floor planOrigin & Audit:
final CatalogueReference? catalogueRef; // Reference to catalogue listing used at creationfinal UserId createdByUserId; // User who created the assetfinal UserId? updatedByUserId; // User who last modified the assetfinal DateTime createdAt; // Creation timestampfinal DateTime updatedAt; // Last modification timestampExtended Data:
final Metadata metadata; // Arbitrary metadata dictionaryfinal CustomFieldMap customFields; // Type-safe custom field storage (sourced from listing or user-added)final AttachmentId? primaryImageId; // Primary image referencefinal List<AttachmentId> attachmentIds; // All associated imagesfinal List<AssetComment> comments; // Discussion/note trail with voice supportKey Methods:
isTopologyBindingStale(int currentSequence): Check if structural assignment is outdated relative to topology changescanAddComment(): Validate comment creation eligibilitycanUpdateComment(CommentId, UserId): Validate comment modification eligibility- Comment query helpers:
getComment(),getCommentsByAuthor(),sortedCommentsByCreated,commentsWithVoiceNotes, etc.
Domain Events
Section titled “Domain Events”The domain produces events that capture significant state changes:
AssetCreatedEvent
Section titled “AssetCreatedEvent”Fired when a new asset is registered. Captures initial state including optional catalogue reference for field inheritance.
AssetCreatedEvent( assetId: TrackableAssetId.fromString('asset-123'), name: AssetNickname.fromString('Conference Room HVAC'), assetType: AssetType.fromCode('hvac_ahu'), structuralAssignment: StructuralAssignment( siteId: SiteId.fromString('site-1'), buildingId: BuildingId.fromString('bldg-1'), level: BuildingLevel.ground, topologySequence: 5, ), createdByUserId: UserId.fromString('user-42'), createdAt: DateTime.now(),)AssetUpdatedEvent
Section titled “AssetUpdatedEvent”Fired when asset properties (name, description, type, structural assignment, layer) change. Note: Custom fields use their own event.
AssetStatusChangedEvent
Section titled “AssetStatusChangedEvent”Fired when operational status transitions (draft → active → inactive → maintenance → decommissioned → deleted). Optionally includes a reason.
AssetStatusChangedEvent( assetId: TrackableAssetId.fromString('asset-123'), previousStatus: AssetStatus.active, newStatus: AssetStatus.maintenance, reason: AssetStatusReason.fromString('annual_inspection'), updatedBy: UserId.fromString('user-42'), updatedAt: DateTime.now(),)AssetPositionUpdatedEvent
Section titled “AssetPositionUpdatedEvent”Fired when geographic position (GeoPosition) or structural assignment changes. Both can be updated independently or together.
AssetLocationUpdatedEvent
Section titled “AssetLocationUpdatedEvent”High-level event tracking location reference changes (used for audit/change tracking).
AssetDeletedEvent
Section titled “AssetDeletedEvent”Fired when an asset is permanently removed (status transition to deleted).
Comment Management Events
Section titled “Comment Management Events”AssetCommentAddedEventAssetCommentUpdatedEventAssetCommentRemovedEvent
Each includes the comment object, actor, and timestamp for complete audit trails.
Image Management Events
Section titled “Image Management Events”AssetImageUploadedEvent: New image attached with metadataAssetImageDeletedEvent: Image removedPrimaryImageSetEvent: Primary image designated
AssetCustomFieldUpdatedEvent
Section titled “AssetCustomFieldUpdatedEvent”Fired when a custom field is modified. Includes the field key, type, new value, and actor.
Structural Assignment & Location Concepts
Section titled “Structural Assignment & Location Concepts”StructuralAssignment Value Object
Section titled “StructuralAssignment Value Object”Represents where an asset is placed within the estate’s structural hierarchy:
class StructuralAssignment { final SiteId siteId; // Always present - root site final BuildingId? buildingId; // Optional: building within site final RoomId? roomId; // Optional: room within building final BuildingLevel level; // Floor/level within building (e.g., G, 1, 2, -1) final int topologySequence; // Version of topology when assigned final DateTime assignedAt; // Assignment timestamp final GeoFeatureKey? drawingFeatureKey; // Optional: link to drawing feature}Key Behaviors:
- Invariant: If
buildingIdis present,levelmust also be present (assets in buildings must be on a level) - Topology Tracking:
topologySequencerecords when the assignment was made, allowing staleness detection if the estate topology changes - Validation:
isValid()andisStale()check assignment currency
GeoPosition Value Object
Section titled “GeoPosition Value Object”Represents geographic location for map-based operations:
class GeoPosition { final GeoJSONPoint point; // RFC 7946 compliant [longitude, latitude] final double? altitude; // Optional elevation in meters final double? accuracy; // Optional horizontal accuracy in meters final DateTime? lastUpdated; // Optional timestamp of measurement}Provides convenience accessors: latitude, longitude, and methods for GeoJSON serialization.
Placement Patterns
Section titled “Placement Patterns”Assets can be positioned via multiple, independent mechanisms:
- Structural: Within the estate hierarchy (required, always a site minimum)
- Geographic: Via GeoJSON coordinates (optional, typically outdoor/site level)
- Layer: Within a floor plan drawing (optional, interior placement visualization)
These patterns coexist. An asset might be assigned to Building A, Room 201, AND have a geographic position if it’s precisely mapped.
Asset Status Lifecycle
Section titled “Asset Status Lifecycle”draft ↓active ←→ inactive ← → maintenance ↓ ↓ └──→ decommissioned → deletedStatus Values:
draft: New asset not yet in serviceactive: Operational and in useinactive: Temporarily not in service (available for reactivation)maintenance: Undergoing service or repairdecommissioned: Permanently out of service but retained for historical recorddeleted: Removed from systemunknown: Fallback for unrecognized values
Asset-Estate Structure Relationships
Section titled “Asset-Estate Structure Relationships”Assets relate to the estate structure through StructuralAssignment:
Estate (SiteId)├── Building (BuildingId)│ ├── Level (BuildingLevel: Ground, 1, 2, -1, etc.)│ │ └── Room (RoomId)│ │ └── Asset (with StructuralAssignment)│ ││ └── (unplaced building - still referenced by assets)│└── Asset (site-level, no building)Key Patterns:
-
Building Required for Level: Assets in buildings must specify a level. Unassigned assets start at ground level by default.
-
Site Always Present: Every asset must reference a site, even if unassigned to buildings.
-
Topology Sequence: The
topologySequenceinStructuralAssignmentrecords the estate structure’s version at assignment time. When estate topology changes (buildings added/removed, rooms reorganized), this sequence increments. Assets with stale sequences can be identified for revalidation. -
Unplaced Buildings: The domain supports assets assigned to “unplaced” buildings (buildings without geographic geometry), allowing structural hierarchy representation independent of physical placement.
Operations & Directives
Section titled “Operations & Directives”The domain implements directives (intent handlers) for all user-initiated operations:
Asset Lifecycle
Section titled “Asset Lifecycle”CreateAssetDirective Creates a new asset, optionally inheriting custom fields from a catalogue listing.
CreateAssetPayload( assetId: TrackableAssetId.fromString('asset-456'), name: AssetNickname.fromString('Emergency Light'), assetType: AssetType.fromCode('emergency_light'), structuralAssignment: StructuralAssignment( siteId: SiteId.fromString('site-1'), buildingId: BuildingId.fromString('bldg-1'), level: BuildingLevel.fromJson({'number': 2}), topologySequence: 5, ), createdByUserId: UserId.fromString('user-42'),)UpdateAssetStatusDirective Transitions asset status (e.g., draft → active).
UpdateAssetStatusPayload( assetId: TrackableAssetId.fromString('asset-123'), previousStatus: AssetStatus.draft, newStatus: AssetStatus.active, updatedBy: UserId.fromString('user-42'),)UpdateAssetPositionDirective Updates geographic and/or structural position.
UpdateAssetPositionPayload( assetId: TrackableAssetId.fromString('asset-123'), geoPosition: GeoPosition.fromLatLng( latitude: 40.7128, longitude: -74.0060, accuracy: 5.0, ), updatedByUserId: UserId.fromString('user-42'),)Topology Assignment
Section titled “Topology Assignment”AssignAssetToTopologyDirective / BulkAssignAssetsToTopologyDirective Assign asset(s) to site/building/room within the estate structure.
AssignAssetToTopologyPayload( assetId: TrackableAssetId.fromString('asset-123'), siteId: SiteId.fromString('site-1'), buildingId: BuildingId.fromString('bldg-2'), roomId: RoomId.fromString('room-205'), level: BuildingLevel.fromJson({'number': 2}), updatedByUserId: UserId.fromString('user-42'),)Comment Management
Section titled “Comment Management”AddAssetCommentDirective Attach a comment (with optional voice note, photo, or video).
AddAssetCommentPayload( assetId: TrackableAssetId.fromString('asset-123'), comment: AssetComment( commentId: CommentId.generate(), content: DomainText.fromString('Serviced filter'), authorUserId: UserId.fromString('user-42'), createdAt: DateTime.now(), voiceNoteId: AttachmentId.fromString('voice-123'), // optional ),)UpdateAssetCommentDirective / RemoveAssetCommentDirective Modify or delete existing comments (author-only by default).
Image Management
Section titled “Image Management”AssetImageUploadedEvent / AssetImageDeletedEvent
Manage image attachments. Use PrimaryImageSetEvent to designate the primary image.
Custom Fields
Section titled “Custom Fields”UpdateCustomFieldDirective Update a single custom field with type safety.
UpdateCustomFieldPayload( assetId: TrackableAssetId.fromString('asset-123'), fieldKey: CustomFieldKey.fromString('manufacturer'), fieldType: CustomFieldType.text, fieldValue: CustomFieldValue.text('ACME Corp'), updatedByUserId: UserId.fromString('user-42'),)Bulk Operations
Section titled “Bulk Operations”BulkDeleteAssetsDirective Delete multiple assets in a single operation.
BulkUpdateAssetPositionsDirective Update positions (e.g., rescale coordinates) for many assets at once (useful for drawing rescale).
BulkCreateAssetsFromListingDirective Create multiple assets from a single catalogue listing, fetching the listing once and applying defaults to all.
Topology Validation
Section titled “Topology Validation”RevalidateAssetTopologyDirective Revalidate asset assignments against current estate topology (after topology changes).
RescaleAssetPositionsDirective Rescale all asset positions following drawing coordinate system changes.
Querying Assets
Section titled “Querying Assets”The domain provides declarative selectors for common query patterns:
import 'package:trackable_asset_v1/trackable_asset_v1.dart';
// Stream assets in a specific topology scopewatchAssetsInTopology( app: nomosApp, workspaceId: workspaceId, timelineId: timelineId, siteId: 'site-1', buildingId: 'bldg-1', roomId: 'room-205', onlyValidAgainstCurrentTopology: true, currentTopologySequence: 10,);
// Stream assets with geographic positionswatchAssetsWithGeo( app: nomosApp, workspaceId: workspaceId, timelineId: timelineId, siteId: 'site-1',);
// Query assets within a geographic polygonqueryAssetsInGeoPolygon( app: nomosApp, workspaceId: workspaceId, timelineId: timelineId, polygon: GeoJSONPolygon(...), siteId: 'site-1',);
// Stream assets with stale topology assignmentswatchAssetsWithStaleTopologyBinding( app: nomosApp, workspaceId: workspaceId, timelineId: timelineId, currentTopologySequence: 10, siteId: 'site-1',);Change Tracking
Section titled “Change Tracking”AssetChangeService Compute detailed asset changes between two points in time:
final service = AssetChangeService(nomosApp);final changes = await service.computeAssetChanges( workspaceId: workspaceId, timelineId: timelineId, siteId: siteId, baselineSequence: 5, currentSequence: 10,);
// Returns ChangeItem records with before/after states and detailed change descriptionsTracks:
- Asset creation, deletion, updates
- Property changes (name, type, status)
- Structural assignment changes (building, room)
- Geographic position changes
- Attachment and comment count changes
- Custom field additions/removals
Custom Fields
Section titled “Custom Fields”Assets support extensible custom fields with type safety:
final customFields = CustomFieldMap({ CustomFieldKey.fromString('manufacturer'): CustomField.text(key, 'ACME Corp'), CustomFieldKey.fromString('warranty_years'): CustomField.number(key, 5), CustomFieldKey.fromString('needs_inspection'): CustomField.boolean(key, true),});Field Sourcing: Custom fields track their origin:
listing: Inherited from catalogue listing at creationuserAdded: Added or modified by user after creation
Type-Aware Defaults: The domain applies asset-type-specific defaults at creation:
hvac_ahu: Quantity (1), Wattage (7500W)fire_extinguisher: Compliance Required (true)
Integration Points
Section titled “Integration Points”Upstream Dependencies
Section titled “Upstream Dependencies”- contracts_v1: Published language for IDs (
TrackableAssetId,SiteId,BuildingId, etc.), asset types, asset status reasons, metadata structures - estate_structures_v1: Site/building/room definitions and topology structure
- catalogues_v1: Listing aggregates for field inheritance
- foundation_geometry_v1: Geometric operations
- nomos_core: Core framework (events, aggregates, directives, GeoJSON via
nomos_geo)
Downstream Consumers
Section titled “Downstream Consumers”- Applications & UIs: Read asset state, dispatch directives, subscribe to events
- Policies & Workflows: Validate operations, orchestrate multi-domain flows
- Analytics: Consume events for reporting and auditing
Validation & Invariants
Section titled “Validation & Invariants”Asset Aggregate Invariants:
- Asset ID cannot be unresolved (unless aggregate is empty)
- Non-deleted assets must have a non-empty name
- Non-deleted assets must have a non-empty type code
- Building-Level Invariant: If
structuralAssignment.buildingIdis present,levelmust also be present
Status Transition Rules: Defined at the policy/application layer, not enforced within the domain.
Complete Example: Asset Workflow
Section titled “Complete Example: Asset Workflow”import 'package:trackable_asset_v1/trackable_asset_v1.dart';import 'package:contracts_v1/contracts_v1.dart';
// 1. Create an asset from a catalogue listingfinal createDirective = CreateAssetDirective( payload: CreateAssetPayload( assetId: TrackableAssetId.generate(), name: AssetNickname.fromString('HVAC Unit 3F'), description: AssetDescription.fromString('Main supply, 3rd floor'), assetType: AssetType.fromCode('hvac_ahu'), structuralAssignment: StructuralAssignment( siteId: SiteId.fromString('site-nyc'), buildingId: BuildingId.fromString('bldg-main'), level: BuildingLevel.floor(3), topologySequence: 15, assignedAt: DateTime.now(), ), catalogueRef: CatalogueReference( listingId: ListingId.fromString('listing-hvac-standard'), workspaceId: NomosWorkspaceId.fromString('workspace-123'), timelineId: NomosTimelineId.fromString('timeline-default'), ), topologySequence: 15, createdByUserId: UserId.fromString('user-alice'), ),);
// 2. Update asset status once operationalfinal statusDirective = UpdateAssetStatusDirective( payload: UpdateAssetStatusPayload( assetId: createDirective.payload.assetId, previousStatus: AssetStatus.draft, newStatus: AssetStatus.active, updatedBy: UserId.fromString('user-bob'), ),);
// 3. Add geographic position (e.g., from GPS)final positionDirective = UpdateAssetPositionDirective( payload: UpdateAssetPositionPayload( assetId: createDirective.payload.assetId, geoPosition: GeoPosition.fromLatLng( latitude: 40.7128, longitude: -74.0060, accuracy: 3.5, ), updatedByUserId: UserId.fromString('user-charlie'), ),);
// 4. Add a maintenance commentfinal commentDirective = AddAssetCommentDirective( payload: AddAssetCommentPayload( assetId: createDirective.payload.assetId, comment: AssetComment( commentId: CommentId.generate(), content: DomainText.fromString('Replaced filter, tested airflow'), authorUserId: UserId.fromString('user-dave'), createdAt: DateTime.now(), ), ),);
// 5. Query assets in the buildingfinal assetsInBldg = watchAssetsInTopology( app: nomosApp, workspaceId: workspaceId, timelineId: timelineId, buildingId: 'bldg-main',).listen((assets) { print('Assets in main building: ${assets.length}');});
// 6. Detect topology stalenessfinal staleAssets = watchAssetsWithStaleTopologyBinding( app: nomosApp, workspaceId: workspaceId, timelineId: timelineId, currentTopologySequence: 16, // topology changed since assignment siteId: 'site-nyc',);Architecture Notes
Section titled “Architecture Notes”- Event Sourcing: The domain is built on event sourcing via nomos_core. State is reconstructed by applying events in sequence.
- Snapshot Optimization: Snapshots are periodically persisted to avoid replaying long event chains.
- Timeline Awareness: All operations are timeline-aware, supporting branched, sandboxed, or alternative timelines.
- Web Compatibility: Safe JSON parsing handles Flutter Web’s JS
undefinedsemantics (see_parseString(),_parseMap()in aggregate).
Related Domains
Section titled “Related Domains”- estate_structures_v1: Defines the site/building/room hierarchy that
StructuralAssignmentreferences - catalogues_v1: Provides listing definitions that assets inherit custom fields from
- contracts_v1: Shared value object and ID types across the system