Skip to content

3. Facets and Entities

An entity is a shared identity composed from facets. A facet owns a coherent slice of state and behavior.

text
ops.area
├── identity      code, name, description
├── tenancy       tenant, site
├── governance    status, actions, audit
└── hierarchy     type, category, parent, capacity

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

dart
final Entity areaEntity = Entity(
  name: 'area',
  title: 'Area',
  pluralTitle: 'Areas',
  description: 'Controlled physical spaces within an operating site.',
  tableName: 'areas',
  schema: 'ops',
  schemaType: 'ops.area',
  tenantField: AreaFields.tenantId,
  versioning: const EntityVersioning.revisions(),
  uniqueKeys: const [
    [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:

dart
uniqueKeys: const [
  [AreaFields.tenantId, AreaFields.code],
],

There is no hidden tenant prepend.

Facet dependencies form a DAG

When one facet requires another:

dart
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 EntityVersioning.versions for immutable point-in-time history and const EntityVersioning.revisions() when the entity also needs governed business revisions, approval context, evidence links, and producing actions. Both use the same monotonically increasing technical 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

  • FacetProjection describes the facet's read/write projection.
  • EntityProjection is assembled from effective facets.
  • DerivedValue belongs to a facet when it depends on that facet.
  • Entity aggregates combine multiple facets.

Stored fields remain stored state. Do not create fake persisted fields for values that are derived from other state.

Validate early

dart
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.

Next: Relationships and projections.

Blue is the Vyuh Blueprint documentation surface.