16. Semantic Composition and Catalogs
The advanced Blueprint style is not a larger constructor call. It is a small domain language made from reusable Dart types.
Reusable facet types keep shared meaning while letting each feature add its own details.
Create semantic facet families
abstract class GovernedFacet extends Facet {
const GovernedFacet({
required super.name,
required super.fields,
super.actions,
});
}
final class CleaningFacet extends GovernedFacet {
const CleaningFacet({super.actions})
: super(
name: 'cleaning',
fields: const [CleaningStatus()],
);
@override
CleaningFacet replacingActions(List<Action> next) =>
CleaningFacet(actions: next);
}
final class CleaningStatus extends TextField {
const CleaningStatus() : super('status');
}The subtype expresses a reusable semantic family while retaining the ordinary Facet contract. Overriding replacingActions preserves the subtype when assembly merges capture defaults and lifted rules.
Let assembly lift shared rules
Rules may be declared at Blueprint, module, entity, or action scope. Assembly lifts broader rules onto mutating actions without changing their phase:
const tenantRule = Rule(
id: 'common.tenant.required',
kind: RuleKind.access,
phase: RulePhase.beforeCommit,
test: Not(
IsNull<String>(FieldValue<String>(FieldOf<String>(TenantIdField()))),
),
);
const blueprint = Blueprint(
name: 'operations',
version: '1.0.0',
rules: [tenantRule],
modules: [opsModule],
);The effective action retains its local rules plus the lifted rule and their provenance. There is one rule vocabulary, not separate global, module, entity, and action rule systems.
Close executable Dart types explicitly
Studio and runtime inspection need to know which concrete semantic subtypes an application supports:
final types = BlueprintTypeCatalog(
modules: const [
BlueprintModuleType<Module>(key: 'core.module'),
],
entities: const [
BlueprintEntityType<Entity>(key: 'core.entity'),
],
facets: const [
BlueprintFacetType<IdentityFacet>(key: 'core.identity'),
BlueprintFacetType<CleaningFacet>(key: 'ops.cleaning'),
],
fields: const [
BlueprintFieldType<CodeField>(key: 'core.code'),
BlueprintFieldType<CleaningStatus>(key: 'ops.cleaning_status'),
],
actions: const [
BlueprintActionType<Action>(key: 'core.action'),
],
);
final unsupported = types.unsupportedTypes(blueprint);Catalog keys are persisted identities. Matching is intentionally by exact runtime type: registering Facet does not silently authorize every custom subclass.
Keep catalogs distinct
Two catalog concepts serve different purposes:
| Catalog | Contains | Purpose |
|---|---|---|
BlueprintTypeCatalog | supported Dart declaration types | prove the runtime understands authored types |
BlueprintCatalog | portable definition documents | search, select, and compose reusable models in Studio |
Do not combine them. A definition catalog does not contain constructors, and a type catalog does not contain reusable business modules.
Checkpoint
Create CleaningFacet and CalibrationFacet as GovernedFacet subtypes. Contribute calibration from a second module through an EntityDescriptor, then prove that the assembled entity carries both facets and the type catalog reports no unsupported declarations.
Next: Executable entity queries.