3. Facets and Entities
An entity is a shared identity composed from facets. A facet owns a coherent slice of state and behavior.
Blueprint deliberately does not put lifecycle or actions directly on Entity. Different facets can carry different state machines, rules, evidence requirements, and action surfaces.
Declare the entity
final Entity areaEntity = Entity(
name: 'area',
title: 'Area',
pluralTitle: 'Areas',
description: 'Controlled physical spaces within an operating site.',
db: const EntityDb(tableName: 'areas'),
tenantField: AreaFields.tenantId,
history: History.revisions(),
uniqueKeys: const [
UniqueKey([AreaFields.tenantId, AreaFields.code]),
],
facets: const [
IdentityFacet(
fields: [
AreaFields.code,
AreaFields.name,
AreaFields.description,
],
),
Facet(
name: 'tenancy',
fields: [AreaFields.tenantId, AreaFields.siteId],
),
Facet(
name: 'hierarchy',
title: 'Hierarchy',
fields: [
AreaFields.areaType,
AreaFields.capacity,
AreaFields.roomNumber,
],
),
],
);Identity is explicit
Every assembled entity requires exactly one IdentityFacet. Composite unique keys must use the same field token objects declared on that identity surface. Tenant-scoped uniqueness is explicit:
uniqueKeys: const [
UniqueKey([AreaFields.tenantId, AreaFields.code]),
],There is no hidden tenant prepend.
UniqueKey is the single semantic home for uniqueness. The database compiler lowers it to DbUniqueConstraint; do not author a duplicate constraint under EntityDb. Use nullsNotDistinct: true only when null values are intended to collide, such as a global row in a scoped-settings table.
Facet dependencies form a DAG
When one facet requires another:
Facet(
name: 'release',
dependsOn: const ['governance', 'quality_review'],
// ...
)The validator rejects missing dependencies and cycles. Dependencies describe semantic ordering; they do not mean “render this below that.” UI order belongs in FacetUI.
Versioned entities retain definitions
Use History.versions() for immutable point-in-time history and History.revisions() when the entity also needs governed entity revisions, approval context, evidence links, and producing actions. Both use the same monotonically increasing entity version sequence. An execution record can then pin the exact version it used.
Version history is not a replacement for the action journal. It is a state projection that makes historical definitions resolvable.
Projections and aggregates
FacetProjectiondescribes the facet's read/write projection.EntityProjectionis assembled from effective facets.DerivedValuebelongs to a facet when it depends on that facet.- Entity
aggregatescombine multiple facets.
Stored fields remain stored state. Do not create fake persisted fields for values that are derived from other state.
Semantic facet types survive composition
A product may subclass Facet to name a reusable semantic family. Assembly composes same-name contributions into EffectiveFacet while retaining every original declaration. EffectiveFacet.declarationsOf<T>() finds authored or descriptor-contributed declarations by Dart type without promoting their members to the entity.
Chapter 17 develops this into reusable semantic atoms and a closed runtime type catalog.
Validate early
final blueprint = Blueprint.single(
name: 'learning_ops',
version: '0.1.0',
schema: 'ops',
entities: [areaEntity],
);
final errors = BlueprintValidator.validate(blueprint);
if (errors.isNotEmpty) {
for (final error in errors) {
print('${error.code} @ ${error.path}: ${error.message}');
}
}Validation is the language type-checking phase. Run it before DDL generation, server bootstrap, or UI assembly.
Checkpoint
Why is governance a facet rather than an entity-level boolean called active? Your answer should mention lifecycle, rules, actions, evidence, and audit.