17. Complete examples
Contents of DISL 0.2
The three specifications below are complete, and each one validates against the JSON Schema of Appendix A. Every CEL expression in them compiles with a reference CEL implementation (cel-go) configured with the DISL library of section 12. Together they exercise most of the language. They are also distributed as separate files next to this specification.
Beside them, seven trimmed excerpts of real diagram specifications show the constructs added in 0.2, one file each, and two further files show what a validator prints and the DID 0.2 value forms. They validate against the schemas like the three above, but are not reproduced here:
| File | What it shows |
|---|---|
statemachine.dis, timeline.dis, erd.dis |
The three complete examples of 17.1 to 17.3. |
mindmap.dis |
Per-viewer folding (11.6), positional create and reorder (7.3, 8.4), branch-or-leaf confirmations (9.5), a text metric (6.5), anchors restricted to sides (6.9), containment drawn as edges (4.11), a file-system rule (12.4), and tolerant ids with built-in messages for missing and duplicate ids (11.5.4). |
c4-container.dis |
View membership with wildcards (3.5), relationships lifted to what is drawn and merged (4.11.3), a legend computed from what is drawn and a header band (6.13), read-only reasons (2.3, 4.3), drops onto the parent kind (7.2), whole-model and per-view rules (8.2), and natural, case-insensitive and ephemeral ids (11.5). |
rdf-graph.dis |
Derived resource cards and statement edges computed from stored triples (4.11), a truncating budget with a declared budget:<id> notice and a budget that gates edits (3.2.1, 6.13), the edit gate and named reasons (9.1), refusals per element kind (6.9, 6.10), derived and ephemeral ids with per-type rules (11.5), bounded recursion (3.4), connect gestures chosen by anchor (6.10), plugin functions called by name (13.1.1) and std.unparseable (8.7). |
gartner-hype-cycle.dis |
The yearMonth time axis with a bound unit and ranges (5.3, 5.5), multi-level and adaptive rulers (5.13), snapping per gesture (5.9), boundary handles that write the model and show a computed label (6.8), edge ends bound to shape parts (6.10), a tag filter (6.13.1), a compact variant (3.5), commit-time field validation with parseYearMonth (7.5, 12.4) and mixed-precision findings (8.7). |
functional-decomposition.dis |
The superellipse and a custom shape (6.7, 6.8), a contrast requirement (6.2), a fixed attribute (4.3), acyclicity over an abstract relation type (4.9), built-in refusals worded by the specification (8.1, 8.4), a deletion confirmation with a threshold (9.5), the findings form item (7.5) and base-36 ids (11.5.1). |
causal-loop.dis |
Cycle enumeration with diagram.cycles and diagram.knots (12.4), one finding per cycle with forEach (8.2), a hook whose actions run per cycle (9.2, 9.4), drops onto a variable that are refused elsewhere (7.2), view membership (3.5) and the empty-canvas message (6.13). |
databricks-job.dis |
A simulated run (9.6), rules per item with forEach and file locations (8.2), std.unreadableEntry (8.7), enum stored forms (4.5), a stub for a relation whose target is optional (4.9, 6.10), Bézier reach (6.10) and context-menu entries generated per item (7.3). |
findings.json |
Headless validator output (8.6), in the object form of $defs/ValidatorOutput: a parse failure with file, line and column, a finding on an undrawn subject, a finding on elements with a location, and a per-view finding. |
../did/time-values.did |
The DID 0.2 value forms: signed yearMonth values before year 1, datetime values kept at their written precision, a relation without a target, and a suppression keyed by subject (DID, section 10). |
This example shows a classic node-and-edge diagram type:
- Metamodel: an abstract
Vertexsupertype, aStatewith containment (composite states), pseudo-states withmultiplicity, a view-onlyComment, aTransitionwith anexpression-typed guard, and anAnchorrelation that may point at other relations (connectsRelations). - Coordinates: one pixel system with a 10 px grid applied to both axes, 10 px size snapping with a minimum of 20, 15° rotation steps and object snapping, all documented for users.
- Notation: theme tokens with a dark mode; a composite
bullseyeshape with a handle that adjusts the ring gap; the parameterisedspeechbubble from 6.8 used for comments; a compartment computed from attributes; curved transitions with a casing, a label whose inline edit is parsed back intotrigger,guardandeffect, a priority label at the start of the edge, and dashed lines for guarded transitions. - Toolbox: grouped tools, a disabled tool once an initial state exists, context tools and a parameterised template.
- Constraints: structural rules (one initial state, reachability, determinism), a
connectgesture rule, and style rules with quick fixes that apply to all findings at once. - Behavior: automatic naming, an operation that groups the selection into a composite state, and deletion and clipboard policies.
- Persistence: split model and view files, prefixed UUIDv7 ids, tree ordering and a chain of migrations.
File statemachine.dis:
{
"$schema": "https://etalii.net/adp/disl/schema/0.2/disl.schema.json#/$defs/Specification",
"disl": "0.2",
"language": {
"id": "org.example.statemachine",
"version": "1.2.0",
"label": { "en": "State machine", "de": "Zustandsautomat" },
"doc": {
"summary": "Finite state machines with triggers, guards and effects.",
"description": "Draw **states** and connect them with **transitions**. Every machine has exactly one initial state. Guards are CEL expressions over the machine's variables."
},
"defaultLocale": "en",
"locales": ["en", "de"],
"fileExtension": "sm.json",
"requires": { "conformance": "standard" }
},
"functions": {
"displayName": {
"params": [{ "name": "e", "type": "Element" }],
"returns": "string",
"cel": "has(e.name) && e.name != '' ? e.name : e.type",
"doc": "Name of a state, or its type when unnamed."
},
"transitionText": {
"params": [{ "name": "t", "type": "Element" }],
"returns": "string",
"cel": "t.trigger + (t.guard != '' ? ' [' + t.guard + ']' : '') + (t.effect != '' ? ' / ' + t.effect : '')",
"doc": "UML-style transition label: trigger [guard] / effect."
}
},
"metamodel": {
"diagram": {
"attributes": {
"title": { "type": "string", "label": "Title" },
"variables": { "type": "Variable", "many": true, "doc": "Variables that guards may read." }
}
},
"dataTypes": {
"Variable": {
"fields": {
"name": { "type": "string", "required": true, "pattern": "^[a-z][A-Za-z0-9]*$" },
"dataType": { "type": "VarKind", "default": "int" }
},
"display": { "cel": "value.name + ': ' + value.dataType" }
}
},
"enums": {
"VarKind": { "values": { "int": {}, "bool": {}, "string": {} } }
},
"types": {
"Vertex": {
"abstract": true,
"doc": "Anything a transition can connect."
},
"State": {
"extends": "Vertex",
"doc": { "summary": "A condition the system can be in.", "tags": ["core"] },
"attributes": {
"name": { "type": "string", "required": true, "maxLength": 60, "unique": "parent",
"doc": "Unique within the enclosing region." },
"entry": { "type": "string", "label": "Entry action", "doc": "Runs every time the state is entered." },
"exit": { "type": "string", "label": "Exit action" },
"color": { "type": "color", "palette": ["#EEEDFE", "#E1F5EE", "#FAEEDA", "#FAECE7"] }
},
"children": { "allowed": ["Vertex"] },
"labelAttribute": "name"
},
"InitialState": { "extends": "Vertex", "multiplicity": { "min": 1, "max": 1 },
"doc": "Where execution starts. Exactly one per machine." },
"FinalState": { "extends": "Vertex", "doc": "Execution ends when a final state is reached." },
"Choice": { "extends": "Vertex", "doc": "Dynamic branch; outgoing guards are evaluated in order." },
"Comment": {
"viewOnly": true,
"attributes": { "text": { "type": "text", "markup": "markdown" } },
"doc": "A note on the diagram. Comments have no meaning for execution."
}
},
"relations": {
"Transition": {
"doc": "A change from one vertex to another, fired by a trigger when the guard holds.",
"source": { "types": ["Vertex"], "exclude": ["FinalState"] },
"target": { "types": ["Vertex"], "exclude": ["InitialState"] },
"allowSelfLoops": true,
"attributes": {
"trigger": { "type": "string" },
"guard": { "type": "expression", "context": "x-guard", "resultType": "bool" },
"effect": { "type": "string" },
"priority": { "type": "int", "default": 0, "min": 0 }
}
},
"Anchor": {
"viewOnly": true,
"source": "Comment",
"target": ["Vertex", "Transition"],
"connectsRelations": true,
"doc": "Attaches a comment to an element."
}
}
},
"coordinates": {
"axes": { "px": { "kind": "linear", "unit": "px" } },
"systems": {
"canvas": {
"kind": "cartesian",
"x": "px",
"y": "px",
"grid": { "visible": true, "style": "dots", "spacing": 10, "majorEvery": 5 },
"snapping": "grid10",
"guides": { "smart": true, "distribution": true }
}
},
"snapProfiles": {
"grid10": {
"both": { "grid": { "spacing": 10 } },
"size": { "both": { "grid": { "spacing": 10 }, "min": 20 } },
"targets": ["objects", "rule"],
"rotation": { "step": 15 },
"doc": { "summary": "Everything aligns to a 10 px grid.", "rationale": "Aligned diagrams are easier to read and diff." }
}
},
"default": "canvas"
},
"notation": {
"theme": {
"tokens": {
"color.surface": "#FFFFFF",
"color.text": "#2C2C2A",
"color.border": "#5F5E5A",
"color.accent": "#534AB7",
"color.accent.soft": "#EEEDFE",
"color.note": "#FAEEDA",
"font.body": "Inter, system-ui, sans-serif",
"font.mono": "JetBrains Mono, monospace"
},
"modes": {
"dark": {
"color.surface": "#2C2C2A",
"color.text": "#F1EFE8",
"color.border": "#B4B2A9",
"color.accent.soft": "#3C3489",
"color.note": "#633806"
}
},
"followSystem": true
},
"styles": {
"base": {
"fill": { "token": "color.surface" },
"stroke": { "color": { "token": "color.border" }, "width": 1.25 },
"font": { "family": { "token": "font.body" }, "size": 13, "color": { "token": "color.text" } }
},
"stateBody": {
"extends": "base",
"fill": { "attribute": "color" },
"cornerRadius": 10
},
"pseudo": { "fill": { "token": "color.text" }, "stroke": { "width": 0 } }
},
"shapes": {
"bullseye": {
"label": "Final state",
"doc": "A filled circle inside a ring.",
"params": { "gap": { "type": "number", "unit": "length", "default": 4, "min": 2, "max": "min(w, h) / 3" } },
"parts": [
{ "id": "ring", "shape": "ellipse", "box": { "x": 0, "y": 0, "w": "w", "h": "h" }, "outline": true,
"style": { "fill": "none" } },
{ "id": "dot", "shape": "ellipse", "box": { "x": "p.gap", "y": "p.gap", "w": "w - 2.0 * p.gap", "h": "h - 2.0 * p.gap" },
"style": { "fill": { "token": "color.text" }, "stroke": { "width": 0 } } }
],
"handles": [
{ "param": "gap", "axis": "x", "x": "p.gap", "y": "h / 2.0", "value": "px", "label": "Ring gap",
"snap": { "grid": { "spacing": 1 } } }
],
"aspectRatio": 1
},
"speech": {
"label": "Speech bubble",
"doc": "Drag the round handle to point the tail, the square handle to change the corner radius.",
"params": {
"tipX": { "type": "number", "unit": "fraction", "default": 0.2, "min": -0.5, "max": 1.5 },
"tipY": { "type": "number", "unit": "fraction", "default": 1.4, "min": -0.8, "max": 1.8 },
"base": { "type": "number", "unit": "fraction", "default": 0.3, "min": 0.1, "max": 0.8 },
"radius": { "type": "number", "unit": "length", "default": 8, "min": 0, "max": "min(w, h) / 2.0" }
},
"parts": [
{ "id": "body", "shape": { "type": "roundedRect", "params": { "radius": { "param": "radius" } } },
"box": { "x": 0, "y": 0, "w": "w", "h": "h" }, "outline": true },
{ "id": "tail",
"shape": { "path": { "segments": [
{ "op": "M", "x": "w * p.base", "y": "h - 1.0" },
{ "op": "L", "x": "w * p.tipX", "y": "h * p.tipY" },
{ "op": "L", "x": "w * (p.base + 0.15)", "y": "h - 1.0" },
{ "op": "Z" } ] } },
"box": { "x": 0, "y": 0, "w": "w", "h": "h" } }
],
"textArea": { "x": 10, "y": 8, "w": "w - 20.0", "h": "h - 16.0" },
"handles": [
{ "param": "tipX", "yParam": "tipY", "axis": "both", "x": "w * p.tipX", "y": "h * p.tipY",
"value": "px / w", "yValue": "py / h", "label": "Tail tip" },
{ "param": "radius", "axis": "x", "x": "p.radius", "y": 0, "value": "px",
"snap": { "grid": { "spacing": 2 } }, "label": "Corner radius" }
]
}
},
"nodes": {
"State": {
"shape": { "type": "roundedRect", "params": { "radius": 10 } },
"style": "stateBody",
"size": { "default": [140, 70], "min": [80, 40], "autoSize": "grow" },
"labels": [
{ "id": "name", "text": { "attribute": "name" }, "position": "top", "editable": "inline",
"style": { "font": { "weight": 600 } }, "placeholder": "Unnamed state" }
],
"compartments": [
{ "id": "actions",
"items": { "cel": "(self.entry != '' ? ['entry / ' + self.entry] : []) + (self.exit != '' ? ['exit / ' + self.exit] : [])" },
"itemText": { "cel": "item" },
"style": { "font": { "family": { "token": "font.mono" }, "size": 11 } },
"separator": { "color": { "token": "color.border" }, "width": 0.5 },
"visible": { "cel": "self.entry != '' || self.exit != ''" } }
],
"container": { "layout": "free", "padding": [36, 12, 12, 12], "autoGrow": true, "highlightOnDrop": true },
"badges": [
{ "id": "hidden", "position": "bottom-right", "shape": "pill", "size": 16,
"text": { "cel": "'+' + string(self.children.size())" },
"visible": { "cel": "self.view != null && self.view.collapsed && self.children.size() > 0" },
"tooltip": "Collapsed composite state: number of hidden sub-states" }
],
"conditions": [
{ "when": "self.children.size() > 0", "style": { "stroke": { "width": 2 } },
"doc": "Composite states have a heavier border." }
],
"states": {
"selected": { "stroke": { "color": { "token": "color.accent" }, "width": 2 } },
"dropTarget": { "fill": { "token": "color.accent.soft" } }
},
"tooltip": { "cel": "displayName(self) + ' — ' + string(self.outgoing.size()) + ' outgoing transitions'" },
"lod": [ { "maxZoom": 0.35, "hide": ["compartments", "badges"] } ]
},
"InitialState": {
"shape": "circle",
"style": "pseudo",
"size": { "fixed": [20, 20] },
"placement": { "anchor": "center" },
"labels": [],
"tooltip": "Initial state"
},
"FinalState": {
"shape": "bullseye",
"style": "base",
"size": { "fixed": [24, 24] },
"placement": { "anchor": "center" },
"labels": []
},
"Choice": {
"shape": "diamond",
"style": "base",
"size": { "fixed": [28, 28] },
"placement": { "anchor": "center" },
"labels": []
},
"Comment": {
"shape": "speech",
"style": { "fill": { "token": "color.note" }, "stroke": { "color": "#BA7517", "width": 0.75 } },
"size": { "default": [180, 70] },
"labels": [ { "id": "text", "text": { "attribute": "text" }, "format": "markdown", "editable": "multiline",
"position": "top-left", "overflow": "ellipsis" } ],
"layer": "foreground"
}
},
"edges": {
"Transition": {
"line": {
"stroke": { "color": { "token": "color.border" }, "width": 1.25,
"casing": { "color": { "token": "color.surface" }, "width": 5 } },
"routing": "curved",
"curvature": 0.2,
"bendpoints": { "editable": true, "removeOnStraighten": true }
},
"targetMarker": { "type": "arrowFilled", "params": { "length": 10, "width": 8 } },
"labels": [
{ "id": "text", "text": { "cel": "transitionText(self)" }, "at": "middle", "side": "above",
"background": { "fill": { "token": "color.surface" }, "padding": 2, "cornerRadius": 3 },
"editable": "inline",
"parse": { "cel": "value", "write": [
{ "set": {
"trigger": "value.split('[')[0].split('/')[0].trim()",
"guard": "value.contains('[') ? value.split('[')[1].split(']')[0].trim() : ''",
"effect": "value.contains('/') ? value.split('/')[1].trim() : ''" } } ] } },
{ "id": "prio", "text": { "cel": "self.priority > 0 ? string(self.priority) : ''" }, "at": "start", "offset": 10,
"side": "below", "style": { "font": { "size": 10 } } }
],
"selfLoop": { "shape": "arc", "size": 36, "side": "top" },
"parallel": { "spread": 14, "mode": "curve" },
"states": { "selected": { "stroke": { "color": { "token": "color.accent" }, "width": 2 } } },
"conditions": [ { "when": "self.guard != ''", "style": { "stroke": { "dash": "dashed" } },
"doc": "Guarded transitions are dashed." } ]
},
"Anchor": {
"line": { "stroke": { "dash": "dotted", "cap": "round", "color": "#BA7517", "width": 1 } },
"targetMarker": "none"
}
}
},
"toolbox": {
"layout": "list",
"searchable": true,
"groups": [
{ "id": "vertices", "label": { "en": "States", "de": "Zustände" }, "tools": [
{ "id": "state", "creates": "State", "shortcut": "S", "icon": "square-rounded" },
{ "id": "initial", "creates": "InitialState", "shortcut": "I", "icon": "circle-dot",
"enabled": "diagram.nodesOfType('InitialState').size() == 0" },
{ "id": "final", "creates": "FinalState", "icon": "circle-dot" },
{ "id": "choice", "creates": "Choice", "icon": "diamond" }
] },
{ "id": "connections", "label": { "en": "Connections", "de": "Verbindungen" }, "tools": [
{ "id": "transition", "creates": "Transition", "mode": "drag", "shortcut": "T", "createTarget": "State" }
] },
{ "id": "annotations", "label": "Annotations", "collapsed": true, "tools": [
{ "id": "comment", "creates": "Comment", "icon": "message" },
{ "id": "anchor", "creates": "Anchor" },
{ "id": "retry", "kind": "template", "template": "retryLoop", "label": "Retry loop" }
] }
],
"contextTools": [
{ "for": ["State", "Choice", "InitialState"], "placement": "around", "tools": [
{ "kind": "create-connected", "creates": "State", "via": "Transition", "direction": "outgoing",
"label": "Add next state", "icon": "arrow-right", "position": "right" },
{ "kind": "connect", "via": "Transition", "icon": "link", "position": "bottom" },
{ "kind": "delete", "position": "top-right" }
] }
],
"templates": {
"retryLoop": {
"label": "Retry loop",
"doc": "A state that retries a number of times before failing.",
"params": { "attempts": { "type": "int", "default": 3, "min": 1, "label": "Attempts" } },
"fragment": {
"nodes": [
{ "ref": "try", "type": "State", "attributes": { "name": "'Trying'" }, "at": [0, 0] },
{ "ref": "fail", "type": "FinalState", "at": [220, 20] }
],
"relations": [
{ "type": "Transition", "source": "try", "target": "try",
"attributes": { "trigger": "'error'", "guard": "'retries < ' + string(p.attempts)" } },
{ "type": "Transition", "source": "try", "target": "fail",
"attributes": { "trigger": "'error'", "guard": "'retries >= ' + string(p.attempts)" } }
]
}
}
}
},
"forms": {
"stateInspector": {
"for": "State",
"usage": ["inspector"],
"items": [
{ "attribute": "name" },
{ "attribute": "color", "widget": "color" },
{ "kind": "section", "title": "Actions", "items": [
{ "attribute": "entry", "widget": "code", "widgetOptions": { "language": "text" } },
{ "attribute": "exit", "widget": "code", "widgetOptions": { "language": "text" } }
] },
{ "kind": "findings" }
]
},
"transitionInspector": {
"for": "Transition",
"items": [
{ "attribute": "trigger", "placeholder": "e.g. open" },
{ "attribute": "guard", "widget": "code", "widgetOptions": { "language": "cel" },
"doc": "Boolean CEL expression over the machine's variables." },
{ "attribute": "effect" },
{ "attribute": "priority", "widget": "spinner",
"visible": "self.source.outgoingOf('Transition').size() > 1" }
]
}
},
"constraints": {
"groups": {
"structure": { "label": "Structure", "doc": "Rules without which the machine cannot run." },
"style": { "label": "Style guide", "enabledByDefault": true }
},
"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 in only one place." },
"fixes": [
{ "label": "Add an initial state", "when": "diagram.nodesOfType('InitialState').size() == 0",
"actions": [ { "create": { "type": "'InitialState'", "at": "[40.0, 40.0]" } } ] }
]
},
{
"id": "initialHasOneExit", "group": "structure", "scope": "InitialState",
"rule": "self.outgoing.size() == 1",
"message": "The initial state must have exactly one outgoing transition."
},
{
"id": "noEntryIntoInitial", "kind": "connect",
"when": "relationType == 'Transition'",
"rule": "!target.isA('InitialState')",
"message": "Transitions cannot enter the initial state."
},
{
"id": "reachable", "group": "structure", "scope": "Vertex",
"when": "!self.isA('InitialState')",
"rule": "diagram.nodesOfType('InitialState').exists(i, self in i.reachable('Transition'))",
"severity": "warning", "cost": "expensive",
"message": { "cel": "'\"' + displayName(self) + '\" can never be reached.'" }
},
{
"id": "deterministic", "group": "structure", "scope": "State",
"rule": "self.outgoingOf('Transition').filter(t, t.guard == '').map(t, t.trigger).isUnique()",
"message": "Two unguarded transitions leave this state on the same trigger.",
"doc": { "rationale": "The machine would not know which transition to take." }
},
{
"id": "choiceNeedsElse", "group": "structure", "scope": "Choice",
"rule": "self.outgoingOf('Transition').exists(t, t.guard == 'else')",
"severity": "warning",
"message": "A choice should have an 'else' branch.",
"fixes": [ { "label": "Mark the last branch as else", "when": "self.outgoing.size() > 0",
"actions": [ { "set": { "guard": "'else'" }, "target": "self.outgoing[self.outgoing.size() - 1]" } ] } ]
},
{
"id": "nameCase", "group": "style", "scope": "State",
"rule": "self.name == '' || self.name.substring(0, 1) == self.name.substring(0, 1).upperAscii()",
"severity": "hint", "attribute": "name",
"message": "State names start with a capital letter.",
"fixes": [ { "label": "Capitalise", "preferred": true, "applyToAll": true,
"actions": [ { "set": { "name": "self.name.substring(0, 1).upperAscii() + self.name.substring(1)" } } ] } ]
}
]
},
"behavior": {
"hooks": [
{ "id": "nameNewState", "on": "create", "for": "State", "when": "!has(self.name) || self.name == ''",
"actions": [ { "set": { "name": "'State ' + string(diagram.nodesOfType('State').size())" } } ],
"doc": "New states are numbered automatically." }
],
"operations": {
"makeFinal": {
"label": "Convert to final state", "for": "State", "icon": "flag",
"enabled": "self.outgoing.size() == 0",
"actions": [ { "retype": { "to": "'FinalState'" } } ]
},
"extractComposite": {
"label": "Group into composite state", "for": "selection",
"enabled": "selection.all(e, e.isA('Vertex'))",
"actions": [
{ "create": { "type": "'State'", "attributes": { "name": "'Composite'" } }, "as": "group" },
{ "forEach": "selection", "as": "v", "do": [ { "reparent": { "target": "v", "parent": "group" } } ] },
{ "layout": { "scope": "[group]" } }
]
}
},
"deletion": { "State": { "children": "reparent", "relations": "delete" } },
"clipboard": { "relations": "internal", "ids": "regenerate", "names": "suffix", "offset": [20, 20] },
"undo": { "mergeWindowMs": 500 }
},
"layout": {
"algorithms": {
"flow": { "algorithm": "layered", "direction": "right",
"spacing": { "node": 40, "layer": 90, "edge": 12 }, "edgeRouting": "curved",
"doc": "Left-to-right flow." }
},
"default": "flow",
"trigger": "onLoadIfMissing",
"respect": "pinned"
},
"persistence": {
"format": "json",
"files": { "mode": "split", "model": "{name}.sm.json", "view": "{name}.sm.view.json" },
"ids": { "strategy": "uuid-v7", "prefix": { "State": "st_", "Transition": "tr_", "Comment": "cm_" } },
"ordering": { "elements": "tree", "keys": "canonical" },
"omitDefaults": true,
"precision": { "canvas": 1 },
"view": { "store": ["bounds", "waypoints", "labelOffsets", "params", "viewport", "pinned"],
"styleOverrides": ["fill"] },
"metadata": ["languageVersion", "createdAt", "modifiedAt", "generator"],
"migrations": [
{ "from": ">=1.0.0 <1.1.0", "to": "1.1.0",
"doc": "Renamed 'label' to 'name'.",
"steps": [ { "renameAttribute": { "type": "State", "from": "label", "to": "name" } } ] },
{ "from": ">=1.1.0 <1.2.0", "to": "1.2.0",
"doc": "EndState became FinalState; transitions got a priority.",
"steps": [
{ "renameType": { "from": "EndState", "to": "FinalState" } },
{ "setAttribute": { "type": "Transition", "attribute": "priority", "value": "0",
"when": "!has(element.attributes.priority)" } },
{ "transform": { "type": "State",
"cel": "element.attributes.with({'entry': element.attributes.?onEntry.orValue('')})" } },
{ "deleteAttribute": { "type": "State", "attribute": "onEntry" } }
] }
],
"export": [ { "format": "svg" }, { "format": "png", "options": { "scale": 2 } } ]
}
}
This example shows the coordinate layer in action. The x axis is a time axis in working days: weekends and holidays are collapsed, the time zone comes from a diagram attribute, and the ruler has several levels with today marked. The y axis is an ordinal axis whose bands are the Resource nodes. Tasks are bound to the model: x ↔ start, x2 ↔ end, y ↔ assignee. Dragging a task changes dates and assignment, never view data, so the view record of a task is empty (see the DID definition in 17.4).
Snapping is different per axis. On x, tasks snap to whole working days using calendar rules. On y, they snap to the centre of a lane. Sizes snap to whole days with a minimum of one day. The task bar is a composite progressBar shape whose progress handle snaps to quarters. Dependencies anchor at the finish and start of bars; variants move the anchors for start-to-start and finish-to-finish links. The link type is shown at the edge start and the lag in the middle, and conditional styles turn a violated dependency red.
File timeline.dis:
{
"$schema": "https://etalii.net/adp/disl/schema/0.1/disl.schema.json#/$defs/Specification",
"disl": "0.1",
"language": {
"id": "org.example.timeline",
"version": "0.4.0",
"label": "Project timeline",
"doc": {
"summary": "Plan tasks and milestones over calendar time, one lane per resource.",
"description": "The horizontal axis is **calendar time** (working days only). Each lane is a **resource**. Tasks snap to whole working days; dragging a task vertically reassigns it."
},
"requires": { "conformance": "standard", "features": ["axis.time", "axis.ordinal", "placement.bound"] }
},
"metamodel": {
"diagram": {
"attributes": {
"title": { "type": "string" },
"projectStart": { "type": "date", "required": true },
"projectEnd": { "type": "date" },
"timezone": { "type": "string", "default": "Europe/Berlin", "doc": "IANA time zone of the plan." },
"holidays": { "type": "date", "many": true, "doc": "Non-working days in addition to weekends." }
}
},
"enums": {
"DependencyKind": {
"values": {
"FS": { "label": "Finish → start", "doc": "Successor starts after predecessor finishes." },
"SS": { "label": "Start → start" },
"FF": { "label": "Finish → finish" }
}
},
"Status": {
"ordered": true,
"values": {
"planned": { "color": "#B4B2A9" },
"active": { "color": "#378ADD" },
"done": { "color": "#639922" },
"blocked": { "color": "#E24B4A" }
}
}
},
"types": {
"Resource": {
"doc": "A person or team; each resource is one lane.",
"attributes": {
"name": { "type": "string", "required": true },
"position": { "type": "int", "default": 0, "doc": "Lane order." },
"capacity": { "type": "number", "default": 1, "min": 0, "unit": "FTE" }
}
},
"Schedulable": {
"abstract": true,
"attributes": {
"title": { "type": "string", "required": true },
"start": { "type": "date", "required": true },
"assignee": { "type": "Resource", "required": true },
"status": { "type": "Status", "default": "planned" }
},
"labelAttribute": "title"
},
"Task": {
"extends": "Schedulable",
"attributes": {
"end": { "type": "date", "required": true },
"progress": { "type": "number", "min": 0, "max": 1, "default": 0, "step": 0.05 }
}
},
"Milestone": { "extends": "Schedulable", "doc": "A zero-duration event." }
},
"relations": {
"Dependency": {
"source": "Schedulable",
"target": "Schedulable",
"acyclic": true,
"allowParallel": false,
"attributes": {
"linkType": { "type": "DependencyKind", "default": "FS" },
"lag": { "type": "int", "default": 0, "unit": "working days" }
}
}
}
},
"coordinates": {
"axes": {
"time": {
"kind": "time",
"label": "Calendar",
"doc": "Working days of the project calendar. Weekends and holidays are collapsed.",
"valueType": "date",
"timezone": { "attribute": "timezone" },
"scale": { "unit": "day", "size": 36 },
"calendar": { "id": "project", "workingDays": [1, 2, 3, 4, 5], "holidays": { "attribute": "holidays" } },
"collapse": "non-working",
"min": { "cel": "diagram.projectStart" },
"zoomLevels": [
{ "label": "Days", "scale": { "unit": "day", "size": 36 } },
{ "label": "Weeks", "scale": { "unit": "day", "size": 12 } },
{ "label": "Months", "scale": { "unit": "day", "size": 4 } }
],
"ruler": {
"position": "top",
"levels": [
{ "unit": "month", "format": "MMMM yyyy" },
{ "unit": "week", "format": "'W'w", "minZoom": 0.3 },
{ "unit": "day", "format": "EEE d", "minZoom": 0.9 }
]
},
"today": { "visible": true, "label": "Today", "style": { "stroke": { "color": "#E24B4A", "dash": "dashed" } } }
},
"lanes": {
"kind": "ordinal",
"label": "Resources",
"categories": { "nodes": "Resource", "orderBy": "self.position" },
"bandSize": "auto",
"minBandSize": 40,
"gap": 2,
"reorderable": true,
"header": { "size": 140, "side": "left" }
}
},
"systems": {
"schedule": {
"kind": "cartesian",
"x": "time",
"y": "lanes",
"grid": {
"visible": true,
"spacing": { "unit": "day" },
"bands": { "alternate": true, "fill": ["#FFFFFF", "#F1EFE8"] }
},
"snapping": {
"x": { "calendar": { "unit": "day", "align": "nearest", "workingTime": true } },
"y": { "bands": { "align": "center" } },
"size": { "x": { "calendar": { "unit": "day" }, "min": "P1D" } },
"feedback": { "showGhost": true, "showValue": true, "highlightBand": true },
"doc": {
"summary": "Tasks snap to whole working days and into resource lanes.",
"rationale": "Plans are made in days; finer precision would suggest accuracy the plan does not have."
}
}
}
},
"default": "schedule"
},
"notation": {
"styles": {
"bar": {
"fill": { "cel": "enumColor('Status', self.status)" },
"fillOpacity": 0.25,
"stroke": { "color": { "cel": "enumColor('Status', self.status)" }, "width": 1 },
"cornerRadius": 4,
"font": { "size": 12 }
}
},
"shapes": {
"progressBar": {
"doc": "A bar whose darker part shows progress.",
"params": { "progress": { "type": "number", "unit": "fraction", "default": 0, "min": 0, "max": 1 } },
"parts": [
{ "id": "track", "shape": { "type": "roundedRect", "params": { "radius": 4 } },
"box": { "x": 0, "y": 0, "w": "w", "h": "h" }, "outline": true },
{ "id": "done", "shape": { "type": "roundedRect", "params": { "radius": 4 } },
"box": { "x": 0, "y": 0, "w": "w * p.progress", "h": "h" }, "when": "p.progress > 0.0",
"style": { "fillOpacity": 0.8, "stroke": { "width": 0 } } }
],
"handles": [
{ "param": "progress", "axis": "x", "x": "w * p.progress", "y": "h", "value": "px / w",
"snap": { "values": [0, 0.25, 0.5, 0.75, 1] }, "label": "Progress" }
]
}
},
"nodes": {
"Resource": {
"shape": "rect",
"style": { "fill": "none", "stroke": { "width": 0 }, "font": { "weight": 600 } },
"labels": [ { "id": "name", "text": { "attribute": "name" }, "position": "left", "editable": "inline" } ]
},
"Task": {
"shape": { "type": "progressBar", "params": { "progress": { "attribute": "progress" } } },
"style": "bar",
"size": { "default": [null, 24], "resizable": "horizontal" },
"placement": {
"system": "schedule",
"x": { "attribute": "start" },
"x2": { "attribute": "end" },
"y": { "attribute": "assignee" },
"anchor": "left",
"movable": { "x": true, "y": true },
"resizable": { "x": true, "y": false },
"stack": { "mode": "pack", "order": "self.start", "rowSize": 28, "growBand": true },
"doc": "Drag to reschedule or reassign; drag the right edge to change the end date."
},
"labels": [
{ "id": "title", "text": { "attribute": "title" }, "position": "left", "editable": "inline",
"overflow": "ellipsis", "style": { "padding": [0, 6] } },
{ "id": "dates", "text": { "cel": "formatDate(self.start, 'd MMM') + ' – ' + formatDate(self.end, 'd MMM')" },
"position": "outside-right", "style": { "font": { "size": 10, "color": "#888780" } },
"visible": { "cel": "env.zoom >= 0.8" } }
],
"tooltip": { "cel": "self.title + ': ' + string(workingDays(self.start, self.end, 'project')) + ' working days'" },
"states": { "selected": { "stroke": { "width": 2 } } }
},
"Milestone": {
"shape": "diamond",
"style": { "fill": "#2C2C2A", "stroke": { "width": 0 } },
"size": { "fixed": [16, 16] },
"placement": {
"system": "schedule",
"x": { "attribute": "start" },
"y": { "attribute": "assignee" },
"anchor": "center",
"doc": "Milestones sit at the start of their day."
},
"labels": [ { "id": "title", "text": { "attribute": "title" }, "position": "outside-right", "editable": "inline" } ]
}
},
"edges": {
"Dependency": {
"line": {
"stroke": { "color": "#5F5E5A", "width": 1 },
"routing": "rounded",
"cornerRadius": 4,
"startDirection": "right",
"endDirection": "left",
"minSegmentLength": 8
},
"anchoring": {
"source": { "mode": "fixed", "points": [ { "id": "finish", "x": 1, "y": 0.5 } ] },
"target": { "mode": "fixed", "points": [ { "id": "start", "x": 0, "y": 0.5 } ] }
},
"targetMarker": { "type": "arrowFilled", "params": { "length": 7, "width": 6 } },
"labels": [
{ "id": "linkType", "text": { "cel": "self.linkType == 'FS' ? '' : self.linkType" }, "at": "start", "offset": 6, "side": "above",
"style": { "font": { "size": 10 } } },
{ "id": "lag", "text": { "cel": "self.lag != 0 ? (self.lag > 0 ? '+' : '') + string(self.lag) + 'd' : ''" },
"at": "middle", "side": "on", "background": { "fill": "#FFFFFF", "padding": 1 }, "style": { "font": { "size": 10 } } }
],
"variants": [
{ "when": "self.linkType == 'SS'",
"anchoring": { "source": { "mode": "fixed", "points": [ { "id": "start", "x": 0, "y": 0.5 } ] } } },
{ "when": "self.linkType == 'FF'",
"anchoring": { "target": { "mode": "fixed", "points": [ { "id": "finish", "x": 1, "y": 0.5 } ] } } }
],
"conditions": [
{ "when": "self.target.start < addWorkingDays(self.source.start, self.lag, 'project')",
"style": { "stroke": { "color": "#E24B4A", "width": 1.5 } },
"doc": "Red when the dependency is violated." }
]
}
}
},
"toolbox": {
"groups": [
{ "id": "plan", "label": "Plan", "tools": [
{ "id": "task", "creates": "Task", "mode": "drag", "shortcut": "T",
"doc": "Drag horizontally in a lane to create a task over those days.",
"initial": { "progress": 0 } },
{ "id": "milestone", "creates": "Milestone", "mode": "click", "shortcut": "M" },
{ "id": "dependency", "creates": "Dependency", "shortcut": "D" }
] },
{ "id": "people", "label": "Resources", "tools": [ { "id": "resource", "creates": "Resource" } ] }
]
},
"forms": {
"task": {
"for": "Task",
"usage": ["inspector", "create"],
"layout": { "columns": 2 },
"items": [
{ "attribute": "title", "colSpan": 2 },
{ "attribute": "assignee", "widget": "reference", "options": { "cel": "diagram.nodesOfType('Resource')" } },
{ "attribute": "status", "widget": "segmented" },
{ "attribute": "start", "widget": "daterange", "widgetOptions": { "endAttribute": "end" }, "colSpan": 2 },
{ "kind": "computed", "label": "Duration",
"value": { "cel": "string(workingDays(self.start, self.end, 'project')) + ' working days'" } },
{ "attribute": "progress", "widget": "slider", "widgetOptions": { "min": 0, "max": 1, "step": 0.05 },
"format": "#0%" }
]
}
},
"constraints": {
"rules": [
{ "id": "endAfterStart", "scope": "Task", "rule": "self.end >= self.start", "attribute": "end",
"message": "A task cannot end before it starts.",
"fixes": [ { "label": "Make it one day long", "actions": [ { "set": { "end": "self.start" } } ] } ] },
{ "id": "dependencyRespected", "scope": "Dependency",
"rule": "self.linkType != 'FS' || self.target.start >= addWorkingDays(has(self.source.end) ? self.source.end : self.source.start, self.lag + 1, 'project')",
"severity": "warning",
"message": { "cel": "'\"' + self.target.title + '\" starts before \"' + self.source.title + '\" is finished.'" },
"target": "[self, self.target]",
"fixes": [ { "label": "Move successor after predecessor", "preferred": true, "when": "has(self.source.end)",
"actions": [
{ "let": { "shift": "workingDays(self.target.start, addWorkingDays(self.source.end, self.lag + 1, 'project'), 'project')" } },
{ "set": { "start": "addWorkingDays(self.target.start, shift, 'project')",
"end": "addWorkingDays(self.target.end, shift, 'project')" },
"target": "self.target" } ] } ] },
{ "id": "noOverbooking", "kind": "placement", "scope": "Task", "enforcement": "report", "severity": "info",
"rule": "diagram.nodesOfType('Task').filter(t, t.id != self.id && t.assignee == self.assignee && t.start <= newBounds.x2 && newBounds.x <= t.end).size() < int(self.assignee.capacity)",
"message": "This resource is already fully booked in that period." },
{ "id": "withinProject", "scope": "Schedulable",
"when": "has(diagram.projectEnd)",
"rule": "self.start >= diagram.projectStart && self.start <= diagram.projectEnd",
"severity": "warning", "message": "Scheduled outside the project period." }
]
},
"behavior": {
"hooks": [
{ "id": "keepDuration", "on": "change", "for": "Task", "attribute": "start", "phase": "after",
"when": "event.source == 'user' && old != null",
"actions": [ { "set": { "end": "addWorkingDays(self.start, workingDays(old.start, old.end, 'project'), 'project')" } } ],
"doc": "Changing the start date in the form keeps the task's duration." },
{ "id": "autoDone", "on": "change", "for": "Task", "attribute": "progress",
"when": "self.progress >= 1.0 && self.status != 'done'",
"actions": [ { "set": { "status": "'done'" } },
{ "notify": { "message": "'\"' + self.title + '\" marked as done.'", "severity": "success" } } ] }
],
"deletion": { "Resource": { "references": "forbid", "confirm": "Remove this resource?" } }
},
"layout": {
"algorithms": {
"lanes": { "algorithm": "lanes", "doc": "Packs tasks into sub-rows within their resource lane; dates never change." }
},
"default": "lanes",
"trigger": "always"
},
"persistence": {
"format": "json",
"ids": { "strategy": "ulid", "prefix": { "Task": "task_", "Milestone": "ms_", "Resource": "res_", "Dependency": "dep_" } },
"ordering": { "elements": "type-then-id" },
"timestamps": { "format": "rfc3339", "timezone": "preserve" },
"view": { "store": ["viewport", "labelOffsets"] },
"collaboration": { "mode": "crdt", "engine": "yjs", "granularity": "attribute", "presence": true },
"export": [ { "format": "pdf", "options": { "fitTo": "page" } } ]
}
}
This example shows notation driven by the model. Columns are child elements listed in a compartment, with conditional item styles and icons. Relationship markers are computed from the cardinality attributes (erZeroOrMany, erOne, …). The line is dashed for non-identifying relationships. Cardinality labels sit at both ends and only appear when zoomed in. Tables align to a 20 px grid while bendpoints use a 10 px grid. Ids are natural (derived from table and column names), persistence is YAML, and a plugin provides SQL export.
File erd.dis:
{
"$schema": "https://etalii.net/adp/disl/schema/0.1/disl.schema.json#/$defs/Specification",
"disl": "0.1",
"language": {
"id": "org.example.erd",
"version": "2.0.0",
"label": "Entity–relationship diagram",
"doc": "Tables with typed columns and relationships in crow's-foot notation."
},
"metamodel": {
"enums": {
"SqlType": {
"extensible": true,
"values": { "uuid": {}, "text": {}, "varchar": {}, "int": {}, "bigint": {}, "numeric": {},
"bool": {}, "date": {}, "timestamptz": {}, "jsonb": {} }
},
"Cardinality": {
"values": {
"one": { "label": "Exactly one" },
"zeroOrOne": { "label": "Zero or one" },
"oneOrMany": { "label": "One or many" },
"zeroOrMany": { "label": "Zero or many" }
}
}
},
"types": {
"Table": {
"attributes": {
"name": { "type": "string", "required": true, "key": true, "pattern": "^[a-z_][a-z0-9_]*$", "unique": "diagram",
"doc": "snake_case table name." },
"schema": { "type": "string", "default": "public" },
"comment": { "type": "text" }
},
"children": { "allowed": ["Column"], "ordered": true, "min": 1,
"slots": { "columns": { "allowed": ["Column"], "min": 1 } } }
},
"Column": {
"attributes": {
"name": { "type": "string", "required": true, "key": true, "unique": "parent" },
"dataType": { "type": "SqlType", "default": "text", "label": "Type" },
"nullable": { "type": "bool", "default": true },
"primaryKey": { "type": "bool", "default": false, "label": "Primary key" },
"references": { "type": "Column", "doc": "Foreign-key target column." }
}
}
},
"relations": {
"Relationship": {
"source": "Table",
"target": "Table",
"directed": false,
"allowSelfLoops": true,
"attributes": {
"name": { "type": "string" },
"sourceCardinality": { "type": "Cardinality", "default": "one" },
"targetCardinality": { "type": "Cardinality", "default": "zeroOrMany" },
"identifying": { "type": "bool", "default": false,
"doc": "Identifying relationships make the parent key part of the child key." }
}
}
}
},
"coordinates": {
"snapProfiles": {
"coarse": { "both": { "grid": { "spacing": 20 } }, "targets": ["objects", "rule"],
"doc": "Tables align to a 20 px grid; edges snap their bendpoints to 10 px.",
"bendpoints": { "both": { "grid": { "spacing": 10 } } } }
},
"systems": {
"canvas": { "kind": "cartesian", "x": { "kind": "linear" }, "y": { "kind": "linear" }, "snapping": "coarse",
"grid": { "visible": true, "style": "lines", "spacing": 20, "majorEvery": 5 } }
}
},
"notation": {
"styles": {
"table": { "fill": "#FFFFFF", "stroke": { "color": "#444441", "width": 1 }, "cornerRadius": 6,
"font": { "family": "Inter, sans-serif", "size": 12 }, "shadow": { "dx": 0, "dy": 1, "blur": 3, "color": "#00000022" } }
},
"nodes": {
"Table": {
"shape": { "type": "roundedRect", "params": { "radius": 6 } },
"style": "table",
"size": { "default": [220, 120], "autoSize": "fitHeight", "resizable": "horizontal" },
"labels": [
{ "id": "name", "text": { "attribute": "name" }, "position": "top", "editable": "inline",
"background": { "fill": "#EEEDFE", "padding": [6, 8] },
"style": { "font": { "weight": 600, "color": "#3C3489" } } }
],
"compartments": [
{ "id": "columns",
"items": { "children": ["Column"], "slot": "columns" },
"itemText": { "cel": "item.name + ' ' + item.dataType + (item.nullable ? '' : ' not null')" },
"itemIcon": { "cel": "item.primaryKey ? 'key' : (has(item.references) ? 'link' : '')" },
"itemConditions": [ { "when": "item.primaryKey", "style": { "font": { "weight": 600, "decoration": "underline" } } } ],
"separator": { "color": "#D3D1C7", "width": 0.5 },
"overflow": "ellipsis", "maxItems": 20,
"editable": { "add": true, "remove": true, "reorder": true, "inline": true },
"emptyText": "No columns" }
],
"anchors": { "mode": "sides", "spread": true },
"form": { "form": "columnsTable", "interaction": "onDoubleClick", "minZoom": 0.8, "fallback": "labels" },
"lod": [ { "maxZoom": 0.4, "hide": ["compartments"] } ]
},
"Column": { "shape": "none", "labels": [] }
},
"edges": {
"Relationship": {
"line": {
"stroke": { "color": "#444441", "width": 1.25,
"dash": { "cel": "self.identifying ? 'solid' : 'dashed'" } },
"routing": "orthogonal",
"cornerRadius": 0,
"avoidNodes": true,
"minSegmentLength": 18,
"startDirection": "normal",
"endDirection": "normal",
"bendpoints": { "editable": true }
},
"jumps": { "style": "gap", "size": 5 },
"sourceMarker": { "cel": "'er' + self.sourceCardinality.substring(0, 1).upperAscii() + self.sourceCardinality.substring(1)" },
"targetMarker": { "cel": "'er' + self.targetCardinality.substring(0, 1).upperAscii() + self.targetCardinality.substring(1)" },
"labels": [
{ "id": "name", "text": { "attribute": "name" }, "at": "middle", "side": "above", "orientation": "upright",
"style": { "font": { "style": "italic", "size": 11 } }, "editable": "inline" },
{ "id": "srcCard", "text": { "cel": "enumLabel('Cardinality', self.sourceCardinality)" }, "at": "start", "offset": 20,
"side": "below", "style": { "font": { "size": 10, "color": "#888780" } }, "visible": { "cel": "env.zoom > 1.2" } },
{ "id": "tgtCard", "text": { "cel": "enumLabel('Cardinality', self.targetCardinality)" }, "at": "end", "offset": 20,
"side": "below", "style": { "font": { "size": 10, "color": "#888780" } }, "visible": { "cel": "env.zoom > 1.2" } }
]
}
}
},
"forms": {
"columnsTable": {
"for": "Table",
"usage": ["embedded", "inspector"],
"layout": { "density": "compact" },
"items": [
{ "attribute": "name" },
{ "kind": "field", "attribute": "comment", "widget": "textarea", "rows": 2 }
]
}
},
"toolbox": {
"groups": [
{ "id": "model", "label": "Model", "tools": [
{ "id": "table", "creates": "Table", "shortcut": "T" },
{ "id": "rel", "creates": "Relationship", "shortcut": "R" },
{ "id": "rel1n", "creates": "Relationship", "variantOf": "rel", "label": "One-to-many",
"initial": { "sourceCardinality": "one", "targetCardinality": "zeroOrMany" } },
{ "id": "rel11", "creates": "Relationship", "variantOf": "rel", "label": "One-to-one",
"initial": { "sourceCardinality": "one", "targetCardinality": "zeroOrOne" } }
] }
],
"contextTools": [
{ "for": ["Table"], "tools": [
{ "kind": "create-child", "creates": "Column", "slot": "columns", "label": "Add column", "icon": "plus", "position": "bottom" }
] }
]
},
"constraints": {
"rules": [
{ "id": "hasPrimaryKey", "scope": "Table",
"rule": "self.children.exists(c, c.primaryKey)",
"severity": "warning", "message": { "cel": "'Table ' + self.name + ' has no primary key.'" },
"fixes": [ { "label": "Add an id column",
"actions": [ { "create": { "type": "'Column'", "parent": "self", "slot": "'columns'",
"attributes": { "name": "'id'", "dataType": "'uuid'", "primaryKey": "true", "nullable": "false" } } } ] } ] },
{ "id": "fkTypeMatches", "scope": "Column", "when": "has(self.references)",
"rule": "self.references.dataType == self.dataType",
"attribute": "dataType",
"message": { "cel": "'Foreign key type ' + self.dataType + ' does not match ' + self.references.parent.name + '.' + self.references.name + ' (' + self.references.dataType + ').'" },
"fixes": [ { "label": "Use the referenced column's type", "actions": [ { "set": { "dataType": "self.references.dataType" } } ] } ] },
{ "id": "pkNotNull", "scope": "Column", "when": "self.primaryKey", "rule": "!self.nullable",
"enforcement": "prevent-and-report", "attribute": "nullable",
"message": "Primary key columns cannot be nullable." }
]
},
"behavior": {
"hooks": [
{ "id": "pkImpliesNotNull", "on": "change", "for": "Column", "attribute": "primaryKey", "phase": "before",
"when": "self.primaryKey", "actions": [ { "set": { "nullable": "false" } } ] }
]
},
"layout": {
"algorithms": { "orthogonal": { "algorithm": "elk:org.eclipse.elk.layered", "direction": "right",
"edgeRouting": "orthogonal", "spacing": { "node": 60, "layer": 120 } } },
"default": "orthogonal"
},
"persistence": {
"format": "yaml",
"ids": { "strategy": "natural", "stable": true },
"ordering": { "elements": "tree" },
"view": { "store": ["bounds", "waypoints", "labelOffsets", "collapsed"] },
"export": [ { "format": "plugin:sql-ddl", "label": "SQL DDL" } ]
},
"plugins": {
"sql-ddl": { "version": "^1.0.0", "provides": ["export"], "required": false,
"doc": "Generates CREATE TABLE statements." }
}
}
A diagram created with the timeline specification of 17.2 is stored as the DID definition timeline.did, shown and explained in DID (DID-specification.md, section 10). Section 17.5 reads it with the processing model.
When the timeline DID definition is loaded (14.2), the runtime:
- finds
language.versionequal to the specification's, so no migrations run; - builds the
lanesaxis from the twoResourcenodes, ordered byposition; - resolves the
timeaxis: origin atprojectStart, zoneEurope/Berlin, weekends and the three holidays collapsed; - places Wireframes from 1 to 9 October in the Design team lane, Visual design after it (the pack algorithm keeps both in one sub-row because they do not overlap), and Build pages in the Frontend lane;
- evaluates constraints:
dependencyRespectedholds for all three dependencies — Visual design starts on Monday 12 October, the first working day after Wireframes ends on Friday 9 October; Build pages starts on Tuesday 3 November, the second working day after Visual design ends on Friday 30 October, as its lag of one day requires; - renders the three-level ruler, and today's line if
env.nowfalls inside the visible range.
If a user then drags Build pages one working day to the left, the calendar snap rule keeps both ends on working days. Because the whole bar moves, the bindings write start = 2026-11-02 and end = 2026-11-26. The keepDuration hook also fires (the user changed start), and it computes the same end date, so the transaction stays consistent. The dependencyRespected constraint now fails, because the task starts before the one-day lag has elapsed. The edge turns red through its conditional style, and the finding offers the quick fix Move successor after predecessor, which shifts the task back by exactly one working day.
Examples explained here
- Entity–relationship diagram (
erd.dis) - State machine (
statemachine.dis) - Project timeline (
timeline.dis)