8. Layer 5 — Constraints
Contents of DISL 0.1
Constraints state what makes a diagram valid. They are written in CEL, evaluated by the runtime (live, on save, or on demand) and by headless validators, and reported to users with explanations and quick fixes.
{
"constraints": {
"groups": {
"structure": { "label": "Structure", "doc": "Rules without which the machine cannot run." },
"style": { "label": "Style guide", "enabledByDefault": false }
},
"rules": [
{
"id": "singleInitial",
"group": "structure",
"scope": "diagram",
"rule": "diagram.nodesOfType('InitialState').size() == 1",
"severity": "error",
"message": "A state machine needs exactly one initial state.",
"doc": { "rationale": "Execution must start somewhere, and only in one place." },
"fixes": [
{ "label": "Add an initial state", "when": "diagram.nodesOfType('InitialState').size() == 0",
"actions": [ { "create": { "type": "'InitialState'", "at": "[40.0, 40.0]" } } ] }
]
}
]
}
}
| Property | Type | Description |
|---|---|---|
rules |
Constraint[] | Ordered list of constraints. |
groups |
map → {label, doc, enabledByDefault, severityOverride} |
Groups users can enable, disable or re-rate as a unit ("rule sets"). |
defaults |
{severity, timing} |
Defaults for rules. |
blockSaveOn |
"never", "error" |
Whether saving is prevented while errors exist. Default "never" (never lose work); DID definitions are saved with problems and remain loadable. |
builtIn |
map built-in id → {severity, enabled} |
Tune the built-in constraints (8.7). |
| Property | Type | Description |
|---|---|---|
id |
identifier | Required, unique. Stored with suppressions and reported in problems. |
label |
LocalizedText | Short name ("Single initial state"). |
doc |
Doc | summary explains the rule; rationale why it exists; description how to comply; examples valid and invalid snippets. Runtimes show it with every reported problem. |
kind |
ConstraintKind | Default "invariant" (8.3, 8.4). |
scope |
"diagram", "node", "relation", "port", "*", TypeRef or TypeRef[] |
Which elements the rule is evaluated for; self is bound to each. diagram evaluates once. Default "diagram". |
when |
Expression | Precondition; the rule is only evaluated where it holds. |
rule |
Expression → bool | The condition that MUST hold. Required. |
severity |
"error", "warning", "info", "hint", or {cel} |
Default "error". |
message |
LocalizedText or {cel} |
Problem text. CEL messages may interpolate values: { "cel": "'State ' + self.name + ' is unreachable'" }. |
target |
"self" or Expression |
Element(s) the problem is attached to; an expression returning an element or list (for example all duplicates). Default self. |
attribute |
path | Attribute to mark in forms. |
timing |
("live", "save", "explicit", "export")[] |
When evaluated. live: after each transaction (debounced); save: before saving; explicit: on "Validate" command; export: before export. Default ["live", "save"]. |
enforcement |
"report", "prevent", "prevent-and-report" |
prevent: a transaction that would make the rule false (for elements it was true for before) is rejected with the message. Default "report" for invariants, "prevent" for gesture kinds. |
fixes |
QuickFix[] | Suggested corrections. |
suppressible |
bool | Users may suppress the problem for a specific element; suppressions are stored in the DID definition (11.7). Default true for warnings and below, false for errors. |
enabled |
bool | Default true (subject to group). |
group |
group id | |
tags |
string[] | |
cost |
"cheap", "expensive" |
Hint: expensive rules (graph traversals) MAY be evaluated with lower frequency or off the main thread. |
Invariants (kind: "invariant") are statements about the current state of the diagram, evaluated for each element in scope:
[
{ "id": "nameRequired", "scope": "State", "rule": "self.name.trim() != ''",
"attribute": "name", "severity": "error", "message": "Every state needs a name." },
{ "id": "uniqueNames", "scope": "State",
"rule": "diagram.nodesOfType('State').filter(s, s.name == self.name).size() == 1",
"severity": "warning", "message": { "cel": "'The name \"' + self.name + '\" is used more than once.'" },
"target": "diagram.nodesOfType('State').filter(s, s.name == self.name)" },
{ "id": "reachable", "scope": "State",
"when": "!self.isA('InitialState')",
"rule": "diagram.nodesOfType('InitialState').exists(i, self in i.reachable('Transition'))",
"severity": "warning", "cost": "expensive",
"message": { "cel": "'State \"' + self.name + '\" can never be reached.'" } },
{ "id": "deterministic", "scope": "State",
"rule": "self.outgoingOf('Transition').map(t, t.trigger).isUnique()",
"severity": "error",
"message": "Two transitions leave this state on the same trigger.",
"doc": { "rationale": "The machine would not know which transition to take." } }
]
Gesture constraints are evaluated before a user action is applied, while the user is still dragging, so the runtime can show a "not allowed" cursor, apply the dropReject state and explain why. Their default enforcement is prevent.
kind |
Evaluated when | Variables (in addition to diagram, env) |
|---|---|---|
connect |
Creating or reconnecting an edge | relationType (string), source, target (elements or ports), sourcePort, targetPort, self (the edge when reconnecting, else null) |
containment |
Dropping or creating an element inside a container | child (element or {type} for new elements), parent, slot |
create |
Creating an element with a tool or operation | elementType (string), parent, position |
delete |
Deleting elements | self, selection (all elements being deleted) |
placement |
Moving or resizing | self, oldBounds, newBounds (in domain values: newBounds.x is a timestamp on time axes), newParent |
change |
Changing an attribute in a form or label | self, attribute, oldValue, newValue |
[
{ "id": "noEntryIntoInitial", "kind": "connect",
"when": "relationType == 'Transition'",
"rule": "!target.isA('InitialState')",
"message": "Transitions cannot enter the initial state." },
{ "id": "noOverlapPerResource", "kind": "placement", "scope": "Task",
"rule": "!diagram.nodesOfType('Task').exists(t, t.id != self.id && t.assignee == self.assignee && t.start < newBounds.x2 && newBounds.x < t.end)",
"enforcement": "report", "severity": "warning",
"message": "This resource already has a task in that period." },
{ "id": "lockedPhase", "kind": "delete", "scope": "Phase",
"rule": "!self.locked", "message": "Unlock the phase before deleting it." }
]
When enforcement is report on a gesture kind, the gesture is allowed and a problem is reported afterwards if the corresponding state still violates the rule.
| Property | Type | Description |
|---|---|---|
label |
LocalizedText or {cel} |
Menu text ("Rename to 'Idle 2'"). |
doc |
Doc | What the fix does. |
when |
Expression | Offer the fix only if true. |
actions |
Action[] | Behavior actions (9.4) executed as one transaction with self bound to the problem's element. |
operation |
operation id | Alternatively run a declared operation. |
preferred |
bool | The fix offered by "fix all" and keyboard shortcut. |
applyToAll |
bool | May be applied to all problems of this constraint at once. |
Evaluation produces problems: { constraint, severity, message, target, attribute, fixes }. Runtimes MUST present problems on the canvas (the invalid/warning states and a badge), in a problems list (sortable, filterable, clicking reveals the element), and in forms (next to attribute). A headless validator reports problems as JSON (an array of {constraintId, severity, message, elementId, attribute, pointer}) and exits non-zero if errors exist.
Metamodel declarations generate built-in constraints automatically. They behave like declared constraints, can be re-rated in constraints.builtIn, and their messages are localised by the runtime.
| Id | Generated from | Default severity / enforcement |
|---|---|---|
std.required |
required: true |
error / report |
std.facets |
min, max, pattern, minLength, … |
error / report (and prevent in forms) |
std.unique |
unique |
error / report |
std.multiplicity |
multiplicity, children.min/max, perType, relation end min/max, port multiplicity |
lower bounds: warning / report; upper bounds: error / prevent |
std.endpoints |
relation source/target, ports, accepts, allowSelfLoops, allowParallel |
error / prevent |
std.containment |
children.allowed, slots |
error / prevent |
std.acyclic |
acyclic: true |
error / prevent |
std.references |
references to missing elements | error / report |
std.axisBounds |
axis min/max, system bounds |
error / prevent |
std.typeExists |
unknown types in loaded DID definitions | error / report (element preserved, 14.3) |
Illustrated by
- Entity–relationship diagram: 3 items in this layer
- State machine: 9 items in this layer
- Project timeline: 4 items in this layer