Skip to content

11. Layer 8 — Persistence

Contents of DISL 0.4

The persistence layer declares exactly how the diagrams users draw are stored as DID definitions: which format, which files, how identifiers are generated, how elements and keys are ordered, which view data is kept, how precise numbers are, how old DID definitions are migrated, and how several people can edit at once. The goal is that any two conforming runtimes produce identical files for the same diagram, that files diff and merge well in version control, and that stored diagrams remain readable for decades. The structure these settings configure, and the rules a writer follows, are specified by DID (DID-specification.md).

A diagram type whose model lives in a file another tool owns (a Structurizr workspace, a Freeplane mind map, a pipeline's YAML) is not stored as a DID definition: its persistence names a binding in FBL, the Format Binding Language (FBL-specification.md), with format: "fbl" (11.2). (informative)

11.1 Overview

{
  "persistence": {
    "format": "json",
    "files": { "mode": "split", "model": "{name}.sm.json", "view": "{name}.sm.view.json" },
    "ids": { "strategy": "uuid-v7", "prefix": { "State": "st_", "Transition": "tr_" } },
    "references": "id",
    "ordering": { "elements": "type-then-id", "keys": "canonical" },
    "omitDefaults": true,
    "precision": { "canvas": 2 },
    "timestamps": { "format": "rfc3339", "timezone": "utc" },
    "view": { "store": ["bounds", "waypoints", "labelOffsets", "collapsed", "zIndex", "rotation", "viewport"],
              "styleOverrides": ["fill", "stroke.color", "font.weight"] },
    "metadata": ["languageVersion", "createdAt", "modifiedAt", "generator"],
    "migrations": [],
    "collaboration": { "mode": "crdt", "engine": "yjs" }
  }
}

11.2 Formats and encoding

Property Type Description
format "json" (default), "yaml", "jsonl", "cbor", "fbl", "plugin:<name>" Serialization. All formats except fbl encode the same logical DID definition (11.4). jsonl writes one element per line, which merges particularly well. cbor is for large binary-efficient storage. fbl: the model is a body in another format, read and written through binding.
binding URI reference or FBL Binding Required when format is "fbl", forbidden otherwise: the binding that reads and writes the model, as <uri>#<name> of an FBL document (FBL-specification.md, section 2.3), or an inline FBL Binding object.
typeMap map binding type → TypeMapEntry (DISL 0.3) Only with format: "fbl": how the binding's types and attributes map onto the metamodel (below).
encoding "utf-8" Only UTF-8 is allowed.
indent int or "tab" Default 2. 0 writes minified JSON.
newline "lf", "crlf" Default "lf".
finalNewline bool Default true.
compression "none", "gzip", "zip-bundle" zip-bundle stores model, views and binary assets (images) in one archive with a manifest.
mediaType string Media type of DID definitions.

YAML output MUST quote strings that would otherwise be read as other types (the "Norway problem": no, yes, on, off, ~, numeric-looking strings, dates) and MUST NOT use anchors, aliases or tags.

When format is "fbl", the stored model is the body the binding describes, not a DID definition, and it is written by the splices FBL defines, keeping the body's own conventions. files, encoding, indent, newline, finalNewline, compression, mediaType, ordering, omitDefaults, precision, timestamps, canonical, metadata and definition MUST NOT be given. ids, view, migrations, collaboration, import and export keep their meaning; view data is stored in the registration FBL defines. Such a specification needs no persistence plugin; the refusal of 13.1 for a missing required plugin applies only to a plugin the binding's reader names.

Type map (DISL 0.3). A binding may name its types and attributes as its format does rather than as the metamodel does: a hype cycle's binding reads a Unit entry, a Graph header and influences as entries with from and to. typeMap maps each binding type, by its name, to a TypeMapEntry ($defs/TypeMapEntry):

Property Type Description
as "diagram", "header", "unreadable" or TypeRef Required. What the binding's entries of this type become.
attributes map binding attribute → attribute name, "source", "target", "id" or null The binding's attribute names mapped to the metamodel's; unlisted attributes keep their names.
hostAttributes string[] Attributes the binding reads and writes that are not attributes of the model.
doc Doc
  • Without typeMap, an FBL type and attribute MUST have the name of a metamodel type and attribute.
  • With it, a reader maps each FBL element to the type as names. as: "diagram" writes its attributes to the diagram's; "header" and "unreadable" keep the entry out of the model, and an unreadable entry is reported by std.unreadableEntry (8.7). An attribute mapped to id is the element's id, and one mapped to null is not in the model.
  • A reference attribute mapped to source or target makes the element a relation of the type as names, with that end. A relation whose mapped end names nothing MUST still be created, with that end unset, and reported by std.references with detail.end naming the end; it is not drawn (6.1), and std.endpoints does not report the missing end.
  • hostAttributes are read and written by the binding but are not attributes of the model: they are not visible in CEL, forms or constraints, and a writer keeps their values.
  • A binding type that typeMap does not list keeps its name, as without typeMap.
"persistence": { "format": "fbl", "binding": "gartner-hype-cycle-graph.fbl#ghg",
  "typeMap": {
    "Graph":       { "as": "header" },
    "Unit":        { "as": "diagram", "attributes": { "value": "unit" } },
    "Trend":       { "as": "Trend", "hostAttributes": ["storedId", "unknownKeys"] },
    "Influence":   { "as": "Influence", "attributes": { "from": "source", "to": "target" }, "hostAttributes": ["storedId", "unknownKeys"] },
    "Unreadable":  { "as": "unreadable" } } }

The entry influences: [{ id: i1, from: steam-engine, to: railways, to-phase: peak, … }] becomes the relation Influence from steam-engine to railways, and unit: year becomes diagram.unit == 'year'.

11.3 Files

Property Type Description
mode "single" (default), "split", "per-element", "bundle" single: one file with model and views. split: model and view data in separate files, so layout edits never touch the model file. per-element: one file per top-level element in a directory plus an index (for very large models edited by teams). bundle: see compression: "zip-bundle".
model, view, index pattern File name patterns; {name} is the diagram's name, {id} and {type} the element's id and type for per-element.
assets {mode, dir} Binary assets: "inline" (base64), "external" (files in dir, referenced by relative URI), "bundle".
lock bool Write a lock file while open (for file-based single-user editing).

11.4 Stored structure

What a stored diagram looks like, independently of format and file split, is specified by DID, the Diagram Definition Language (DID-specification.md, section 3): the element, relation and view records, and the node and edge view data. Every DID definition names the DISL specification it was made with in its language object. The settings in this section and in 11.5–11.7 are declared by a specification and configure that structure; DID specifies what a conforming writer produces from them.

11.5 Identifiers

persistence.ids ($defs/IdStrategy) says how the elements of a diagram are identified: a rule that applies to every type, and, in types, rules for single types that override it. Each rule is an IdRule ($defs/IdRule); types, missing and compare are allowed only at the top level.

Property Type Description
strategy "uuid-v4", "uuid-v7" (default), "ulid", "nanoid", "sequential", "natural", "cel", "derived" How ids are made (DID, section 4). uuid-v7 and ulid are time-ordered, which keeps id-sorted files in creation order and helps merges. sequential uses per-type counters (State_1) and is only suitable for single-user editing. natural derives ids from key attributes (unique by constraint). cel evaluates expression once, on creation. derived computes the id from the model with expression, whenever the model is read or changed (11.5.2).
encoding "hex" (default), "base64url", "base36" Text form of uuid-v4 and uuid-v7 ids (11.5.1).
prefix string or map TypeRef → string Type prefixes ("st_"), making ids self-describing in diffs. A map is allowed only at the top level. Not applied to the results of cel and derived, whose expression writes any prefix itself.
expression Expression Required for cel and derived. For cel: context create, must return a string. For derived: context identity (12.3), must return a string.
stable bool Ids never change once assigned, even if natural keys change (the first derived id is kept). Default true. Has no effect on derived ids, which always follow their expression.
pattern regex Allowed id syntax; default ^[A-Za-z0-9_.:#-]{1,128}$.
suffix string containing {n} How a generated natural or cel id is made unique. Default "-{n}", the 0.1 behaviour (-2, -3, …).
ephemeral bool or Expression → bool The id identifies the element within one reading of the model only (11.5.3). An Expression is evaluated in the identity context. Default false.
reason Reason The refusal shown when a gesture would store an ephemeral id (11.5.3); context element, with self bound to the ephemeral element.
types map TypeRef → IdRule Per-type rules (11.5.2). Top level only.
missing "assign" (default), "ephemeral" What a runtime does with an element read without a usable id (11.5.4). Top level only.
compare "exact" (default), "ignore-case" How ids are compared (11.5.4). Top level only.
doc Doc

11.5.1 Generation

  • Ids MUST be treated as opaque strings. A runtime MUST NOT decode an id to recover a UUID, a time or a key.
  • A hand-written id that matches pattern MUST be accepted whatever the strategy; the strategy governs only the ids the runtime makes.
  • encoding gives the text form of uuid-v4 and uuid-v7 ids. hex is the RFC 9562 hyphenated lowercase form (the 0.1 form). base64url is the 16 bytes in RFC 9562 byte order encoded with base64url (RFC 4648, section 5) without padding: 22 characters. base36 is the 128-bit value as an unsigned integer in lowercase base 36, left-padded with 0 to 25 characters.
  • A natural id MUST be the type's prefix followed by the string forms of its key attributes, in declaration order after inheritance is flattened, joined by |.
  • When a generated natural or cel id is already in use, the runtime MUST append suffix with {n} replaced by the smallest integer n ≥ 2 that makes the id unique.
  • An id generated for a new element MUST be generated once, when the transaction is first applied. Redoing the transaction MUST re-create the element with the same id, and a redo that would create a duplicate id MUST be refused.
"ids": { "strategy": "uuid-v4", "encoding": "base36", "stable": true }
"ids": { "strategy": "natural", "stable": true, "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", "suffix": "{n}", "compare": "ignore-case" }

The first is the functional decomposition graph's ShortGuid, 25 base-36 characters; the second gives new C4 elements the identifiers web, web2, web3, compared without regard to case as the DSL does.

11.5.2 Per-type rules and derived ids

  • A rule in types MUST apply to the named type and to every subtype that has no rule of its own; where two inherited rules apply, the one nearest in the type's linearisation (4.7) wins. Properties a rule leaves out are taken from the top-level rule.
  • For the derived strategy, the id of an element MUST be the value of expression, evaluated in the identity context (12.3). It MUST be recomputed after the model is read and in the "recompute derived" step of every transaction, before invariants are checked (14.4).
  • A runtime MUST compute ids in this order: nodes, ancestors before descendants, then relations. An identity expression MUST NOT read the id of an element other than an ancestor of self or, for a relation, its ends and their ancestors; a validator MUST reject one that visibly does. A runtime MUST report an expression that fails, or that reads an id not yet computed, as std.missingId on self (8.7).
  • The id of an element of a derived node type or of a derived relation type (4.11) MUST be its derived.id (for a relation without one, the default of 4.11.3), not the id rule's. A rule in types that applies to such a type contributes only ephemeral and reason (11.5.3); its strategy, expression, prefix, encoding, suffix and stable are ignored, and a validator SHOULD warn when a rule for a derived type sets one of them.
  • A derived id MUST NOT be altered to make it unique; two elements with the same derived id are reported by std.duplicateId (11.5.4).
  • When a derived id changes in a transaction, the runtime MUST rewrite every stored reference to it (parent, relation ends, reference attributes, view keys) in the same transaction. When an id read from storage differs from its recomputed value, the element takes the computed id, every reference follows, and the change is written with the next save; it is not an undo step.
  • A rename or move that changes what a derived id is computed from changes the id, so stored view data keyed by the old id no longer applies (informative: that is intended for path ids).

Paths, IRIs, triples with a repeat counter, term forms, scope paths, relation formula ids and fixed singleton ids are all written as derived rules; they differ only in the expression:

"ids": {
  "strategy": "natural", "pattern": "^(res:.+|blank:[0-9]+|edge:.+|truncation)$",
  "types": {
    "Resource":   { "strategy": "derived", "expression": "'res:' + self.iri" },
    "BlankNode":  { "strategy": "derived", "expression": "'blank:' + string(self.ordinal)", "ephemeral": true,
                    "reason": "That is a blank node, whose identity does not survive a reparse, so a stored position could not be trusted. Name it with an IRI to arrange it." },
    "Statement":  { "strategy": "derived",
                    "expression": "cel.bind(n, self.positionIn(diagram.relationsOfType('Statement').filter(r, r.source.id == self.source.id && r.predicateIri == self.predicateIri && r.target.id == self.target.id)), 'edge:' + self.source.id + '|' + self.predicateIri + '|' + self.target.id + (n > 0 ? '|' + string(n) : ''))" },
    "Truncation": { "strategy": "derived", "expression": "'truncation'" }
  }
}

A relation formula id reads its ends, for example "expression": "'depends:' + self.source.id + '->' + self.target.id" for a .NET project reference, and a path id reads the attribute the reader filled, for example "expression": "'playbook:' + self.path".

11.5.3 Ephemeral ids

An element is ephemeral when the rule that applies to its type has ephemeral true, or an ephemeral expression that evaluates to true for it. Its id identifies it within one reading of the model only: blank nodes, anonymous terms, elements numbered by their position in the file. The word ephemeral is used because stable already has its 0.1 meaning.

  • A runtime MUST NOT store, for an ephemeral element, view data (11.6; DID, section 5; or a registration's layout, which is FBL's), a style override, a suppression (8.6), or a reference by its id (a parent, a relation end, or a reference attribute).
  • A gesture whose effect would be one of those writes (moving, resizing or pinning an ephemeral node, connecting to it where the relation would be stored by id, suppressing one of its findings) MUST be refused before it is applied, with the rule's reason or, without one, a runtime sentence saying that the element's identity does not survive re-reading.
  • An ephemeral element MAY be selected, shown in forms, and targeted by findings and operations within the session.
  • Stored view data, style overrides and suppressions keyed by an id that the current reading makes ephemeral MUST be ignored, MUST be reported by std.ephemeralViewData (8.7), and MUST be dropped when they are next written.
  • A relation read from the model that ends at an ephemeral node is not a reference by id; the reader writes it back in the model's own syntax (FBL). DISL forbids only storage keyed by an ephemeral id.
  • A specification whose stored elements are referred to by id (a DID definition, 11.4) SHOULD NOT declare a stored type's ids ephemeral, and a validator SHOULD warn when it does.
"Shape": { "strategy": "derived", "expression": "self.blank ? 'blank:' + string(self.ordinal) : 'res:' + self.iri",
           "ephemeral": { "cel": "self.blank" } }

11.5.4 Missing and duplicate ids

  • Nodes, relations and views of one diagram MUST share one id space. Ports are in the id space of their element when ids are written as element#port.
  • A runtime MUST open a model in which ids are missing or duplicated, and MUST report each case through std.missingId or std.duplicateId (8.7). It MUST NOT refuse the model for it.
  • Of the elements that share an id, the first in reading order (8.6) MUST keep it: every lookup and reference resolves to it. The second and later MUST be drawn, and MUST be treated as ephemeral (11.5.3) until the duplication is resolved.
  • With missing: "assign", an element without a usable id (absent, empty, or not matching pattern) MUST receive a new id from the rule that applies to its type. The new id MUST NOT be written when the model is opened or validated, and MUST be written with the next save that writes the model. With missing: "ephemeral", such an element MUST be drawn and treated as ephemeral.
  • A runtime MUST NOT change an id in order to resolve a duplicate, except through a user action such as a quick fix of std.duplicateId.
  • With compare: "ignore-case", ids MUST be compared using Unicode default case folding for lookups, references, duplicate detection and view keys, and the spelling of the first occurrence in reading order MUST be kept. With compare: "exact", ids compare as code-point sequences, as in 0.1.
"ids": { "strategy": "uuid-v4", "prefix": "ID_", "missing": "assign" }

A mind map node without an id gets ID_ and a new id in memory, written with the next save and never by opening the file or by the headless validator.

11.6 View data and style overrides

view.store lists which kinds of view data are persisted: bounds, waypoints, anchors, labelOffsets, collapsed, zIndex, rotation, params, ports, viewport, guides, pinned, settings, and (DISL 0.4) groups (the collapse states of a node's compartment groups, 6.9, by element and group key) and filters (the values of the canvas filters with persist, 6.13.1). Anything not listed is recomputed on load (by layout or defaults). A pure layout-driven language stores nothing but the model. DID, section 5, specifies the stored form.

view.styleOverrides is "none" (default), "all", or a list of style property paths users may override per element ("fill", "stroke.color", "stroke.dash", "font.size"). Overrides are stored in NodeView.style / EdgeView.style and have the highest precedence (6.1).

No view data or style override is stored for an element with an ephemeral id (11.5.3); stored data keyed by one is ignored, reported by std.ephemeralViewData and dropped on the next write.

Viewer state. Some view data belongs to the person looking, not to the diagram: which branches of a mind map are folded, which filters are set, whether the compact variant is shown. persistence.view gains:

Property Type Default Description
viewer ViewKind[] [] The view-data kinds each viewer holds for themselves. Kinds ($defs/ViewKind): those of store, plus selection, filters and viewpoint.
initial map kind → value or {cel} the kind's default The value a viewer starts with, evaluated in the element context when a viewer first opens the view; collapsed without one starts false.
revealExpands bool true Revealing or selecting an element inside a collapsed container expands its collapsed ancestors.

Viewer state is the value of every kind in view.viewer, of every transient: "viewer" attribute (4.3), of canvas filters (6.13.1), of the variant shown (3.5), of dismissed notices and of a fitted zoom (6.13).

  • A kind MUST NOT appear in both view.store and view.viewer.
  • Viewer state MUST NOT be written to a DID definition or by a format binding, MUST NOT enter the undo or redo history, MUST be changeable in read-only mode, and MUST NOT be shared with another viewer. A runtime MAY keep it for the viewer's session; it MUST NOT outlive the view unless the host keeps it as a per-user preference, which DISL does not define.
  • Viewer state MUST NOT be read in the deterministic contexts of 12.5: self.view for a kind in view.viewer, a viewer attribute, filterValue() and diagram.drawn are rejected there by validators. Findings never depend on a viewer.
  • self.view.collapsed (12.2) reads the viewer's value, and a view action (9.4) sets it.
"persistence": {
  "view": { "store": [], "viewer": ["collapsed", "viewport"],
            "initial": { "collapsed": { "cel": "self.folded" } },
            "doc": "Folding belongs to each viewer: FOLDED in the file seeds it once." }
}

A mind map's folds are each viewer's own, seeded from the file's folded flags the first time a viewer opens it; folding is allowed in read-only mode and is never undone. Without viewer, kinds not in store are recomputed on load, as in 0.1.

View data in the body (DISL 0.4). With format: "fbl", view data is stored in the registration FBL defines (11.2), unless view.bind binds a stored kind onto entries of the body itself, which suits a file whose view data travels with it. bind maps a kind to a ViewBind ($defs/ViewBind):

Property Type Description
type binding type Required. The binding's type whose entries hold this kind.
element binding attribute The attribute naming the element the entry is about, a reference by id. Required except for filters.
x, y binding attribute pinned: the stored position, in the coordinates of the node's placement.
group, collapsed binding attribute groups: the group key (6.9) and its stored state.
values map filter id → binding attribute filters: the attribute holding each kept filter's value.
doc Doc
  • The kinds that may be bound are pinned (an element is pinned exactly while an entry names it, and its position is the entry's), groups and filters. A bound kind MUST be listed in view.store.
  • A bound type is not part of the model: it MUST NOT be a metamodel type or a typeMap key with as naming one, its entries are not elements, and CEL reads them only as view data (self.view.pinned, a group's state, filterValue()). An entry is identified by the element it names and, for groups, the group key; where a host needs an id for it, it is pinned:<element> or groups:<element>#<key>. For filters, the type MAY be a type typeMap maps to the diagram (as: "diagram"), whose bound attributes are then mapped to null.
  • Changing bound view data is planned as the add, set or remove of such an entry (FBL section 6.4), in the same edit as the gesture, and is undone with it. An entry for an element that does not exist, or a second entry for the same key, is ignored and MUST NOT be reported; the runtime removes it with its next write of the body, and removing an element removes its entries in the same edit.
  • Entries change when the body changes from outside like any other entry (FBL section 7.3): the view follows them.
"view": { "store": ["pinned", "groups", "filters"],
  "bind": {
    "pinned":  { "type": "Placement",  "element": "element", "x": "x", "y": "y" },
    "groups":  { "type": "GroupState", "element": "element", "group": "group", "collapsed": "collapsed" },
    "filters": { "type": "Board", "values": { "archived": "showArchived" } } } }

A board's file keeps a pinned item as placements: [{ element: i7, x: 240, y: -80 }], a group opened against its default as groups: [{ element: i7, group: pending, collapsed: false }], and the archived switch as the root key showArchived.

11.7 Ordering, precision and canonical form

Property Type Description
ordering.elements "creation", "id", "type-then-id", "tree", "cel" Order of elements and relations. tree writes parents before children (depth-first, siblings by order). Default "id".
ordering.orderBy Expression For cel: sort key per element.
ordering.keys "canonical", "alphabetical" Key order within objects. canonical: the order of this specification's tables (id, type, parent, slot, order, attributes, ports; then x-); attribute keys in metamodel declaration order.
omitDefaults bool Do not write attribute values equal to their default, view sizes equal to the notation default, or empty collections. Default true.
precision {canvas, numbers} Decimal places for canvas coordinates (default 2) and for number attributes without precision facet (default: shortest round-trip representation).
timestamps {format, timezone} format: "rfc3339" (default) or "epoch-ms"; timezone: "utc" (normalise to Z) or "preserve" (keep the offset entered).
canonical bool Follow RFC 8785 (JSON Canonicalization Scheme) number and string serialization. Default true.

A conforming writer applies these settings as DID, section 6, specifies, including its determinism requirement: the same logical DID definition and specification MUST produce byte-identical output. Suppressions of findings are stored as DID, section 3, describes.

11.8 Metadata and specification embedding

metadata lists the metadata a DID definition carries in meta: languageVersion (always written in language.version), createdAt, modifiedAt, generator, authors (from env.user if the user consents), title (mirrors a diagram attribute), checksum.

definition controls how DID definitions refer to their specification: { "embed": "none" | "reference" | "inline", "uri": "https://…/statemachine-1.2.0.dis" }. reference writes language.definition with the URI of the specification and its integrity hash; inline embeds the whole specification (self-contained archives). The property names definition are kept unchanged from the earlier format.

11.9 Migrations

Migrations upgrade DID definitions written with older language versions. They run on load, before validation, in version order.

{
  "migrations": [
    {
      "from": ">=1.0.0 <1.1.0", "to": "1.1.0",
      "doc": "Renamed 'label' to 'name'; transitions got explicit priorities.",
      "steps": [
        { "renameAttribute": { "type": "State", "from": "label", "to": "name" } },
        { "setAttribute": { "type": "Transition", "attribute": "priority", "value": "0", "when": "!has(element.attributes.priority)" } }
      ]
    },
    {
      "from": ">=1.1.0 <2.0.0", "to": "2.0.0",
      "steps": [
        { "renameType": { "from": "EndState", "to": "FinalState" } },
        { "transform": { "type": "State", "cel": "element.attributes.with({'entryAction': element.attributes.?entry.orValue('')})" } },
        { "deleteAttribute": { "type": "State", "attribute": "entry" } },
        { "convertView": { "system": "canvas", "x": "value * 2.0", "y": "value * 2.0" } }
      ]
    }
  ]
}
Step Fields Effect
renameType from, to Changes type of elements, relations and references.
renameAttribute type, from, to Renames stored attribute keys (including in subtypes).
deleteType type, mode ("delete", "keep-as-unknown") Removes elements of a type (and attached relations).
deleteAttribute type, attribute
setAttribute type, attribute, value (Expression), when Sets a value, for example a default for a new required attribute.
convertAttribute type, attribute, cel Converts a value; value is the old value.
transform type, cel Returns a replacement attributes map; element is the raw element record as a map.
retype type, to, when Changes the type conditionally.
convertView system, x, y, w, h Converts stored view coordinates (unit or origin changes).
renamePort, renameSlot
plugin name, args Custom migration.

Migration expressions run in the migration context (12.3), which sees raw records as maps (element, value, document) rather than typed elements, because the old DID definition does not conform to the new metamodel. A migration that fails aborts loading; the original file is never modified until the user saves. Runtimes SHOULD tell users that a diagram was migrated and to which version, and MAY keep a backup.

DID definitions with a newer minor or patch language version than the loaded specification are opened with a warning; newer major versions are opened read-only or refused.

11.10 Collaboration

Property Type Description
mode "none" (default), "lock", "ot", "crdt" Single user; pessimistic element locks; operational transformation; conflict-free replicated data types.
engine "yjs", "automerge", "plugin:<name>" CRDT or OT engine.
granularity "element", "attribute" Conflict unit. attribute merges concurrent edits of different attributes of one element.
text "replace", "merge" Concurrent edits of text/string attributes: last writer wins or character-level merge.
ordering "fractional-index" Order keys for ordered children and lists that merge without renumbering.
presence bool Share cursors and selections.
conflicts "last-writer-wins", "report" Whether semantic conflicts (both sides valid, the merge invalid) are reported as findings attributed to the merge.

The CRDT state is a transport concern; the persisted DID definition remains the canonical form (DID, section 6). Constraints are re-evaluated after merges.

11.11 Import and export

persistence.export and persistence.import declare conversions: [{ "format": "svg" | "png" | "pdf" | "did-fragment" | "plugin:<name>", "label", "doc", "options" }]. did-fragment writes a DID fragment (DID, section 7). Image exports render the notation exactly as on screen (without interaction states) at a chosen scale; pdf honours canvas.page. Constraints with timing: "export" run before export.

Illustrated by

Referenced from