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:

  1. id attribute on <object> — used by draw.io as the cell identifier

  2. bausteinsicht_id attribute on <object> — redundant copy that survives if draw.io reassigns cell IDs

<object label="REST API" id="webshop.api"
        bausteinsicht_id="webshop.api"
        bausteinsicht_kind="container">
  <mxCell ... />
</object>

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:

  1. bausteinsicht_id attribute (most reliable)

  2. id attribute on <object> (fallback if bausteinsicht_id missing)

  3. 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 <object> with label="" and container=1 on the <mxCell>, plus child text sub-cells (title, tech, desc) from the template. Set bausteinsicht_id, bausteinsicht_kind, technology, tooltip. Place side-by-side with a visual marker (strokeColor=#FF0000;dashed=1;). If element has a detail view, add link="data:page/id,view-<viewID>" for drill-down navigation.

Updated element (title)

Update the title sub-cell’s value attribute (or the label attribute on legacy elements). Only the changed field is overwritten; other fields retain their current draw.io values.

Updated element (description)

Update tooltip attribute on <object> and the description sub-cell’s value.

Updated element (technology)

Update technology attribute on <object> and the technology sub-cell’s value. Remove the tech sub-cell if technology becomes empty.

Updated element (kind)

Update bausteinsicht_kind attribute and apply the new kind’s template style.

Deleted element

Remove <object>, its child text sub-cells, and all connectors where source or target matches the element’s cell ID.

New relationship

Create <mxCell> with edge="1", source, target, and label. Use id="rel-<from>-<to>-<index>". No exitX/entryX (auto-routing). Use parent="1" (layer, not container).

Updated relationship (label)

Update value attribute on the connector <mxCell>.

Deleted relationship

Remove the connector <mxCell>.

New view

Create new <diagram> element with id="view-<viewID>" and view title. Populate all resolved elements and relationships. Add back-navigation button if the view has a scope and a parent view exists.

Deleted view

Remove the orphaned <diagram> page from the draw.io document (#143).

Reverse Sync (draw.io → Model)

Triggered when the draw.io file changes.

Change Type Detection Method Action

Element renamed

Title sub-cell value differs from model title (falls back to parsing HTML label on legacy elements)

Update model element title. Empty titles are rejected (#150).

Element description changed

tooltip on <object> differs from model description

Update model element description

Element technology changed

technology attribute on <object> differs from model

Update model element technology

New element (from draw.io)

<object> wrapping a vertex <mxCell> with no bausteinsicht_id and a non-empty label

Generate ID via sanitizeID (lowercase, hyphen-separated from title). Default kind = first alphabetically-sorted kind from specification.elements (#206). Collision guard: skip import if generated ID already exists in the model (#203). Navigation buttons (nav-back-*) are excluded from import (#205). Bare <mxCell> elements without an <object> wrapper are excluded.

New relationship (from draw.io)

<mxCell> with edge="1", source and target matching known element IDs, not present in model

Add relationship to model. Connector label captured from value attribute (#204).

Element deleted in draw.io

Known bausteinsicht_id no longer present in any <object> on visible view pages

Remove from model. Clean stale references from view include/exclude lists. Display warning listing removed elements.

Relationship deleted in draw.io

Connector with known source/target pair no longer present on any visible view page

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 from/to), preserving kind, label, and description (#185).

Element moved (position only)

<mxGeometry> x/y/width/height changed but no metadata changed

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

  1. Look up the scope element’s kind in the model (e.g., system)

  2. Derive the boundary kind by appending _boundary (e.g., system_boundary)

  3. Look up the boundary template style in the template set

  4. Create the boundary element with the scope element’s title, technology, and description

  5. 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.

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):

  1. Pass 1 — Direct relationships: Only non-lifted relationships are processed. Each direct relationship records its from→to pair in a seen-set.

  2. Pass 2 — Lifted relationships: Only lifted relationships are processed. A lifted relationship is skipped if:

    • Its from→to pair already exists from a direct relationship in Pass 1 (direct suppresses lifted).

    • Its from→to pair 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:

  1. Find the bounding box of all existing elements on the page

  2. Place new elements in a row below the existing content

  3. Use a fixed spacing (40px horizontal gap, 40px below existing)

  4. 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:

  1. Create the container as a swimlane style element

  2. Create children with parent="<container-id>"

  3. Child coordinates are relative to the container’s top-left (offset by startSize header height)

  4. Connectors between elements always use parent="1" (the layer)

Reverse Sync

When detecting containment from draw.io:

  1. If a shape has parent pointing to another shape (not "1"), it is a child

  2. Map the parent-child relationship to the model hierarchy

  3. 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_id that 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_id is 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

title

Element title (always present)

description

Element description (omitted if empty)

technology

Technology label (omitted if empty)

kind

Element kind from the specification (e.g., "container", "system")

RelationshipState Fields
Field Description

from

Source element ID

to

Target element ID

index

0-based position in the model’s relationship array (for disambiguation)

label

Relationship label (omitted if empty)

kind

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

sync algorithm

Template Application

During forward sync, new elements receive their visual style and sub-cell styles from a template.

Template Lookup

  1. Read the template draw.io file

  2. Find elements marked with bausteinsicht_template="<kind>" on their <object>

  3. Extract the style string, width, and height from the template element’s <mxCell>

  4. Find child <mxCell> elements with parent matching the template’s id — these define sub-cell styles for title (-title), technology (-tech), and description (-desc)

  5. 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."