Entity constraints and invariants
The constraints the platform enforces about ontology shapes, state machines, and registration.
This page is a single-page reference for the rules the runtime enforces about the entities you register. Most are checked at registration time so misconfiguration fails at startup, not in production.
Identifier formats
| Identifier | Regex | Example |
|---|---|---|
| Module namespace | ^[a-z][a-z0-9-]*$ | lending, borrower-portal |
| Object type | ^[A-Z][a-zA-Z0-9]*$ | Offer, Vehicle |
| Action ID | ^[a-z][a-z0-9-]*\.[a-z][a-z0-9_]*$ | lending.accept_offer |
| Policy ID | ^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)*\.v[0-9]+$ | lending.credit_pull_consent.v1 |
| Fabric ID | ^[a-z][a-z0-9]{1,7}_[0-9A-Z]{26}$ | act_01HYZAB… |
Action invariants
Enforced at registerAction:
- Action IDs are unique. Re-registering the same ID throws. Use
registerActionIfMissingfor idempotent boot paths. - Mutating actions emit events.
mutatesDomain: true && emitsEvents.length === 0throws. - Action namespace prefix. When registered through
registerFabricModule, the action ID must start with<module.namespace>..
Policy invariants
Enforced at registerPolicy:
- Policy ID format. Must end with
.v<integer>. - No conflicting registrations. Registering a different evaluator for the same ID throws.
- Same evaluator re-registration is a no-op. Idempotent re-registration of the same evaluator object is safe.
Enforced at runtime by evaluateDataPolicyDefinition:
| Bound | Limit |
|---|---|
| Maximum tree depth | 5 |
| Maximum conditions per node | 20 |
| Maximum total conditions in a policy | 100 |
| Maximum path segments | 12 |
| Maximum string literal length | 500 |
| Allowlisted context paths | tenantId, spaceId, actionInvocationId, actionId, mode |
These bounds prevent accidentally deploying a policy that takes seconds to evaluate.
State-machine invariants
Enforced by validateTransition:
- The entity type must have a registered state machine.
- Both
fromandtomust be declared states. - The
(from, to, actionId)triple must be a declared transition.
Stricter than typical state-machine libraries: the action causing the transition is part of the rule. OfferState.presented → accepted via lending.accept_offer is allowed; the same transition via lending.expire_stale_offer is not.
Module invariants
Enforced at registerFabricModules:
- No duplicate namespaces.
- No self-dependency.
- No cyclic dependencies.
- All declared dependencies must resolve to a registered module (or already be registered).
- Display renderers must reference event types known to the platform or declared by the same/another module in the batch.
Event invariants
- Event types are registered before use. Emitting an unknown event type fails (verified by the event-append API).
- Per-subject sequences are monotonic. Replay engines flag gaps via
missing_sequencewarnings. - Event IDs are unique. Replay deduplicates; storage layer enforces.
Adapter invariants
- Adapter
(adapterType, operation)pairs are unique in the registry. Re-registration throws. - Non-idempotent adapters cannot retry. The runtime forces
maxAttempts: 1regardless of policy configuration. - Retry cap. Idempotent adapters are capped at
maxAttempts: 5even if a higher number is configured.
Where these are tested
packages/platform/boundary.test.ts— vertical-vocabulary leakage into the platform.packages/platform/modules/*.test.ts— manifest validation.packages/api/modules/{your-application,borrower-portal}/boundary.test.ts— direct-mutation and direct-handler-import bans.packages/adapters/boundary.test.ts— vertical imports in adapters.
See also
- Stages → state-machine validation
- Where invariants live — decision matrix for new code.
- Policy model — the contract behind the bounds.