<object label="REST API" id="webshop.api"
bausteinsicht_id="webshop.api"
bausteinsicht_kind="container">
<mxCell ... />
</object>
Sync Specification
Overview
Bausteinsicht synchronizes bidirectionally between:
-
JSON Model (source of truth for structure and metadata)
-
draw.io XML (source of truth for layout/positioning)
The sync process reads both files, detects changes relative to a last-sync state, and applies non-conflicting changes in both directions.
Validation Before Sync
Before any sync cycle begins, the model is validated using model.Validate().
If validation errors are found (e.g., invalid view include/exclude patterns, dangling references), sync aborts with an error message listing all validation failures.
This prevents silent data loss from typos such as a trailing dot in an element ID ("customer.") which would otherwise silently remove elements from draw.io (#176).
ID Mapping Strategy
Element IDs
Every element in the JSON model has a unique key (variable name) that serves as its ID.
Nested elements use dot notation: webshop.api.auth.
In draw.io, the ID is stored in two places for reliability:
-
idattribute on<object>— used by draw.io as the cell identifier -
bausteinsicht_idattribute on<object>— redundant copy that survives if draw.io reassigns cell IDs
When views are used, cell IDs are scoped to ensure file-wide uniqueness: <viewID>--<elementID> (e.g., context—customer).
The bausteinsicht_id attribute always stores the un-scoped element ID.
Duplicate bausteinsicht_id Handling
When multiple <object> elements share the same bausteinsicht_id (e.g., across different view pages), the first occurrence encountered wins during draw.io element extraction.
Duplicate occurrences are silently skipped (#213).
Relationship IDs
Connector cells use a derived ID: rel-<from>-<to>-<index> where <index> is the 0-based position of the relationship in the model’s relationship array.
The index disambiguates multiple relationships between the same element pair (#142).
<mxCell id="rel-customer-webshop.api-0" value="uses"
edge="1" source="customer" target="webshop.api" ... />
When multiple relationships exist between the same pair (e.g., customer → webshop.api at indices 0 and 3), each gets a distinct connector ID:
<mxCell id="rel-customer-webshop.api-0" value="uses" ... />
<mxCell id="rel-customer-webshop.api-3" value="manages" ... />
ID Matching Priority
During reverse sync, elements are matched in this order:
-
bausteinsicht_idattribute (most reliable) -
idattribute on<object>(fallback if bausteinsicht_id missing) -
Unmatched elements with no Bausteinsicht attributes are treated as new
Sync Directions
Forward Sync (Model → draw.io)
Triggered when the JSON model changes.
| Change Type | Action |
|---|---|
New element |
Create |
Updated element (title) |
Update the title sub-cell’s |
Updated element (description) |
Update |
Updated element (technology) |
Update |
Updated element (kind) |
Update |
Deleted element |
Remove |
New relationship |
Create |
Updated relationship (label) |
Update |
Deleted relationship |
Remove the connector |
New view |
Create new |
Deleted view |
Remove the orphaned |
Reverse Sync (draw.io → Model)
Triggered when the draw.io file changes.
| Change Type | Detection Method | Action |
|---|---|---|
Element renamed |
Title sub-cell |
Update model element title. Empty titles are rejected (#150). |
Element description changed |
|
Update model element description |
Element technology changed |
|
Update model element technology |
New element (from draw.io) |
|
Generate ID via |
New relationship (from draw.io) |
|
Add relationship to model. Connector label captured from |
Element deleted in draw.io |
Known |
Remove from model. Clean stale references from view include/exclude lists. Display warning listing removed elements. |
Relationship deleted in draw.io |
Connector with known |
Remove relationship from model. |
Relationship direction swap |
Deleted(a→b) paired with Added(b→a) detected in draw.io changes |
Update the existing relationship in-place (swap |
Element moved (position only) |
|
No model change. Layout is a draw.io concern. |
Label Format: Text Sub-Cells
New elements use grouped text sub-cells instead of a single HTML label. The parent element has label="" and three child <mxCell> elements for title, technology, and description. Each sub-cell has independent font size, color, weight, and positioning — matching PlantUML-level typography quality.
<!-- Parent element: label is empty -->
<object label="" id="containers--webshop.api" ...>
<mxCell style="...;container=1;" vertex="1" parent="1">
<mxGeometry x="200" y="150" width="240" height="150" as="geometry" />
</mxCell>
</object>
<!-- Child sub-cells (parent references the element's cell ID) -->
<mxCell id="containers--webshop.api-title" value="REST API"
style="text;html=1;fontSize=14;fontStyle=1;fontColor=#ffffff;..." vertex="1"
parent="containers--webshop.api">
<mxGeometry x="0" y="20" width="240" height="30" as="geometry" />
</mxCell>
<mxCell id="containers--webshop.api-tech" value="[Spring Boot]"
style="text;html=1;fontSize=11;fontStyle=2;fontColor=#CCCCCC;..." vertex="1"
parent="containers--webshop.api">
<mxGeometry x="0" y="55" width="240" height="20" as="geometry" />
</mxCell>
<mxCell id="containers--webshop.api-desc" value="Handles all HTTP requests"
style="text;html=1;fontSize=10;fontColor=#BBBBBB;..." vertex="1"
parent="containers--webshop.api">
<mxGeometry x="0" y="80" width="240" height="40" as="geometry" />
</mxCell>
Sub-cell rules:
-
Title sub-cell: Always created. Plain text (no HTML formatting needed).
-
Technology sub-cell: Only created when technology is non-empty. Value is
[bracketed]. -
Description sub-cell: Only created when description is non-empty.
-
All sub-cells use
movable=0;resizable=0;deletable=0;editable=0;to prevent accidental manipulation.
During reverse sync, ReadElementFields extracts values from sub-cells first. If no sub-cells are found, it falls back to parsing the HTML label (backward compatibility).
Legacy HTML Label Format (Backward Compatible)
Older draw.io files may use a single HTML label attribute instead of sub-cells. The HTML label parser (ParseLabel) supports these formats:
<!-- Title only -->
<b>REST API</b>
<!-- Title + Technology -->
<b>REST API</b><br><font color="#CCCCCC"><i>[Spring Boot]</i></font>
<!-- Title + Technology + Description -->
<b>REST API</b><br><font color="#CCCCCC"><i>[Spring Boot]</i></font><br><font color="#BBBBBB" style="font-size:11px">Handles all HTTP requests</font>
Legacy color codes (#666666 for technology, #999999 for description) are also recognized for backward compatibility.
Empty Title Rejection
Reverse sync rejects empty title updates from draw.io. If a user clears the label in draw.io, the update is ignored and a warning is emitted (#150).
Scope Boundary Handling
When a view defines a scope element, the scope element is rendered as a boundary/swimlane on the view page.
Boundary Creation
-
Look up the scope element’s kind in the model (e.g.,
system) -
Derive the boundary kind by appending
_boundary(e.g.,system_boundary) -
Look up the boundary template style in the template set
-
Create the boundary element with the scope element’s title, technology, and description
-
The scope element is included in the view’s element filter so that connectors targeting the boundary element are rendered correctly (#217)
Child Parenting
Children of the scope element (identified by dot-notation prefix, e.g., shop.api is a child of shop) are parented to the scope boundary cell.
Their parent attribute on <mxCell> is set to the scoped cell ID of the boundary, so they appear visually inside the swimlane.
Child coordinates are relative to the boundary’s top-left.
Drill-Down Navigation
Elements that have a detail view (a view whose scope matches the element) receive interactive drill-down navigation.
Forward Link
On any view page where the element appears, the link attribute is set to data:page/id,view-<viewID>.
Clicking the element in draw.io navigates to its detail view (#198).
Back-Navigation Button
Detail views (views with a scope) get a back-navigation button that links to the parent view.
The parent view is identified as any view whose resolved element set includes the scope element.
The button is an <object> with:
-
id="nav-back-<viewID>" -
label="← <parentViewTitle>" -
link="data:page/id,view-<parentViewID>" -
A small rounded rectangle at position (20, 20) with
width=140,height=30
Back-navigation buttons are excluded from reverse sync element import (#205).
Lifted Connector Deduplication
When a view does not include an endpoint of a relationship, the endpoint is "lifted" to the nearest visible ancestor in the element hierarchy.
For example, if the model has a relationship shop.api → db but only shop and db are on the view, the connector is lifted to shop → db.
Self-Referencing Relationships
True self-loops (where from == to in the model) are rendered as connectors.
However, when lifting causes both endpoints to collapse to the same element (e.g., shop.api → shop.db lifted to shop → shop), the resulting self-reference is skipped because it does not represent a meaningful relationship at the view’s abstraction level (#111, #212).
Two-Pass Deduplication
Relationships are processed in two passes to ensure correct deduplication (#197):
-
Pass 1 — Direct relationships: Only non-lifted relationships are processed. Each direct relationship records its
from→topair in a seen-set. -
Pass 2 — Lifted relationships: Only lifted relationships are processed. A lifted relationship is skipped if:
-
Its
from→topair already exists from a direct relationship in Pass 1 (direct suppresses lifted). -
Its
from→topair already exists from another lifted relationship (first lifted wins, duplicates deduplicated).
-
This ensures that when a direct relationship (e.g., api→db) and a lifted relationship (e.g., api.catalog→db lifted to api→db) map to the same pair, only one connector is created with the direct relationship’s label (#142, #197).
Placement Strategy for New Elements
New elements from forward sync are placed using a simple side-by-side algorithm:
-
Find the bounding box of all existing elements on the page
-
Place new elements in a row below the existing content
-
Use a fixed spacing (40px horizontal gap, 40px below existing)
-
Set a visual marker on new elements (
strokeColor=#FF0000;dashed=1;)
New elements from reverse sync (added in draw.io) keep their draw.io position.
Container Handling
Forward Sync
When generating a view that includes a container element and its children:
-
Create the container as a
swimlanestyle element -
Create children with
parent="<container-id>" -
Child coordinates are relative to the container’s top-left (offset by
startSizeheader height) -
Connectors between elements always use
parent="1"(the layer)
Reverse Sync
When detecting containment from draw.io:
-
If a shape has
parentpointing to another shape (not"1"), it is a child -
Map the parent-child relationship to the model hierarchy
-
If a new element is placed inside a container in draw.io, add it as a child of that container in the model
View Reconciliation
After forward sync applies element and relationship changes, a reconciliation step removes elements from a view page that are no longer in the view’s resolved element set. This handles cases where view include/exclude rules change without corresponding model element changes (#102).
Reconciliation Rules
-
Elements with a
bausteinsicht_idthat is not in the view’s resolved set are removed along with their connectors. -
The scope boundary element is excluded from reconciliation (handled separately).
-
Elements without a
bausteinsicht_id(user-added shapes in draw.io) are preserved — they are not subject to view filtering (#115). -
In no-views mode, orphaned elements (whose
bausteinsicht_idis not in the model at all) are removed (#110).
Orphaned View Page Removal
When views are deleted from the model, their corresponding draw.io <diagram> pages are removed.
Pages are identified as view-managed if their id starts with the "view-" prefix.
Non-view pages (e.g., default template pages without the prefix) are preserved (#143).
New Page Population
When a new view is added to the model, forward sync creates the <diagram> page and populates it with all elements from the view’s resolved set.
This is necessary because elements that already exist in the sync state are not emitted as "Added" in the ChangeSet — they are only detected as changes relative to the last-sync state.
The population step iterates over the resolved element set and creates any element not already present on the page.
The scope boundary element is excluded (handled by createScopeBoundary separately).
Elements expected only on newly created pages are excluded from draw.io-side deletion detection to prevent false "deleted from draw.io" changes (#184, #188, #189).
Conflict Detection
Last-Sync State File
A .bausteinsicht-sync file stores the state after each successful sync:
{
"timestamp": "2026-02-28T12:00:00Z",
"model_hash": "sha256:abc123...",
"drawio_hash": "sha256:def456...",
"elements": {
"webshop.api": {
"title": "REST API",
"description": "Handles all HTTP requests",
"technology": "Spring Boot",
"kind": "container"
}
},
"relationships": [
{
"from": "customer",
"to": "webshop.api",
"index": 0,
"label": "uses",
"kind": "sync"
}
]
}
ElementState Fields
| Field | Description |
|---|---|
|
Element title (always present) |
|
Element description (omitted if empty) |
|
Technology label (omitted if empty) |
|
Element kind from the specification (e.g., |
RelationshipState Fields
| Field | Description |
|---|---|
|
Source element ID |
|
Target element ID |
|
0-based position in the model’s relationship array (for disambiguation) |
|
Relationship label (omitted if empty) |
|
Relationship kind (omitted if empty) |
This file should be committed to version control alongside the model and draw.io files.
Three-Way Merge Logic
With the last-sync state, changes are detected as:
-
Model change: model differs from last-sync, draw.io matches last-sync
-
draw.io change: draw.io differs from last-sync, model matches last-sync
-
Conflict: both model and draw.io differ from last-sync for the same field
-
No change: both match last-sync
Conflict Resolution (v1)
For the initial version, conflicts produce warnings:
WARNING: Conflict detected for element "webshop.api":
Field: title
Model value: "REST API v2"
draw.io value: "Backend API"
Last sync: "REST API"
-> Keeping model value. Edit draw.io manually if needed.
In v1, the model value wins on conflict. This is the safer default since the JSON model is the declared source of truth for structure.
JSONC Comment Preservation
When reverse sync writes changes back to the model file, a PatchSave approach is used to preserve JSONC comments, formatting, and key ordering.
PatchSave Approach
For simple field value changes (title, description, technology), PatchSave locates the target value in the raw JSONC text by walking the JSON path and replaces only the value bytes.
The rest of the document — including // single-line comments, /* */ block comments, whitespace, and key ordering — is preserved.
For structural changes (new elements, new relationships), InsertObjectEntry and AppendArrayEntry are used to insert new JSON entries before the closing } or ] of the target container, preserving surrounding comments and detecting indentation from context.
When structural changes cannot be applied via patching (e.g., element deletion), a full Save is used which re-serializes the model and does not preserve comments.
Sync Algorithm
Template Application
During forward sync, new elements receive their visual style and sub-cell styles from a template.
Template Lookup
-
Read the template draw.io file
-
Find elements marked with
bausteinsicht_template="<kind>"on their<object> -
Extract the
stylestring,width, andheightfrom the template element’s<mxCell> -
Find child
<mxCell>elements withparentmatching the template’sid— these define sub-cell styles for title (-title), technology (-tech), and description (-desc) -
Apply element style and sub-cell styles to the new element
Template Element Example
<!-- In the template file -->
<object label=""
id="template-container"
bausteinsicht_template="container">
<mxCell style="rounded=1;whiteSpace=wrap;html=1;
fillColor=#438DD5;strokeColor=#3C7FC0;container=1;"
vertex="1" parent="1">
<mxGeometry x="0" y="0" width="240" height="150" as="geometry" />
</mxCell>
</object>
<!-- Sub-cell style templates -->
<mxCell id="template-container-title" value="Title"
style="text;html=1;fontSize=14;fontStyle=1;fontColor=#ffffff;..."
vertex="1" parent="template-container">
<mxGeometry x="0" y="20" width="240" height="30" as="geometry" />
</mxCell>
<mxCell id="template-container-tech" value="[Technology]"
style="text;html=1;fontSize=11;fontStyle=2;fontColor=#CCCCCC;..."
vertex="1" parent="template-container">
<mxGeometry x="0" y="55" width="240" height="20" as="geometry" />
</mxCell>
<mxCell id="template-container-desc" value="Description"
style="text;html=1;fontSize=10;fontColor=#BBBBBB;..."
vertex="1" parent="template-container">
<mxGeometry x="0" y="80" width="240" height="40" as="geometry" />
</mxCell>
The bausteinsicht_template attribute identifies which element kind this template applies to.
The element style, dimensions, and sub-cell styles (font size, color, position) are all copied to new elements of that kind.
Connector Template
Relationship connectors also use template styles. The connector template is identified by bausteinsicht_template="relationship" on the <mxCell>:
<!-- In the template file -->
<mxCell bausteinsicht_template="relationship"
style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;
endArrow=block;endFill=1;strokeColor=#666666;"
edge="1" parent="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
|
Note
|
Unlike element templates which use <object> wrappers, the connector template is a bare <mxCell> since connectors do not need custom metadata attributes.
|
Edge Cases
Element appears in multiple views
The same element ID may appear in multiple <diagram> pages.
Each page has its own <object> with independent position and a scoped cell ID (<viewID>--<elementID>).
Changes to metadata (title, description) in one view propagate to all views during sync.
Orphaned connectors
If a connector’s source or target references an element not present on the current page, the connector is removed with a warning.
Empty views
A view with no matching elements generates a <diagram> with only the base cells and a text note: "No elements match this view’s include criteria."
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.