Skip to content

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.

8.1 Overview

{
  "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).

8.2 Constraint object

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.

8.3 Invariants

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." } }
]

8.4 Gesture constraints

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.

8.5 Quick fixes

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.

8.6 Problems

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.

8.7 Built-in constraints

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

Referenced from