finos / finos/architecture-as-code

Layout Persistence Parity Between CALM Hub and VS Code Plugin

Open
#2,953 4 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
399
Forks
138
Avg merge
2d 14h
Merged PRs (30d)
37

Description

## Feature Proposal

**Background**

PR [#2942 ](https://github.com/finos/architecture-as-code/pull/2942)("Layout Persistence in CALM Hub") was merged to main on 2026-08-06. It introduced:

- GET/PUT endpoints at .../namespaces/{namespace}/architectures/{architectureId}/layout
- Flat storage model: one MongoDB document per (namespace, architectureId) in a layouts collection (schema version 9→10)
- Frontend useDefaultLayout hook with slug→numeric resolution, save/reset actions, and a three-tier precedence (local scratch > server default > auto-layout)
- LayoutService, MongoLayoutStore, NitriteLayoutStore, LayoutStoreProducer, MongoLayoutIndexStep
- Architecture-existence guard on PUT, audit trail integration, namespace-deletion guard
- ADR 0005 documenting why the layout shape is Hub-internal, not a CALM schema

This feature request builds directly on that foundation to close the remaining parity gap with the VS Code plugin's layout handling.

---
**Problem Statement**

CALM Hub UI and the VS Code plugin both render architecture diagrams using the same ReactFlow components, but their layout persistence formats diverge — the Hub stores only node positions (x, y) while the VS Code
plugin stores full geometry (x, y, w, h). This means:

- A layout saved in CALM Hub cannot reproduce the exact rendering the VS Code plugin produces (and vice versa)
- Without width/height, ReactFlow auto-sizes nodes based on content, causing layout drift across clients, browser widths, and font rendering differences
- Container (group) nodes cannot be faithfully restored without explicit dimensions — reflowContainersToFitChildren guesses boundaries rather than applying saved ones

---
1. Enhanced Layout Persistence (include width & height)

Current Model (Hub — as shipped in PR #2942)

{
"for": "/api/calm/namespaces/finos/architectures/5",
"pins": [
{ "unique-id": "node-a", "position": { "x": 93, "y": 175 } },
{ "unique-id": "node-b", "position": { "x": 72, "y": 339 } }
]
}

- Array of objects, scanned by unique-id to find a node → O(n)
- No dimensions — position-only, rendering is non-deterministic
- Stored as a parsed BSON Document inside the layouts collection
- Conversion layer: pinsToStoredPositions / storedPositionsToPins in calm-hub-ui/src/model/layout.ts

Proposed Model (aligned with VS Code plugin)

{
"for": "/api/calm/namespaces/finos/architectures/5",
"nodes": {
"actor-123": { "x": 93, "y": 175, "w": 44, "h": 63 },
"service-456": { "x": 72, "y": 339, "w": 109, "h": 33 },
"database-789": { "x": 97, "y": 457, "w": 56, "h": 63 }
}
}

- Map keyed by node unique-id → O(1) lookup
- Full geometry — deterministic rendering across all clients
- Identical shape to what the VS Code plugin already persists in metadata._layout
- Backward-compatible: the for, name, description envelope fields remain unchanged; only pins → nodes with an expanded value shape

**Performance Implications**


Image

The reflowContainersToFitChildren bypass is the real performance win: for architectures with many nested containers, the current code traverses the entire node tree to recompute parent bounds from restored child
positions. With explicit dimensions, containers restore at their saved size with zero computation.

---
2. **UI Layout Source Selection**: _layout (in-document) vs Layout API (server-side)

The VS Code plugin persists layout as metadata._layout within the architecture document itself. When an architecture published to CALM Hub contains this field, the Hub UI should recognise it and offer it as a
layout source alongside the server-side saved layout.

**Current Behaviour (PR #2942)**

1. Local scratch (localStorage) ← user's unsaved drag
2. Layout API (server-side default) ← shared team layout
3. Auto-layout (dagre) ← no layout exists

**Proposed Behaviour**

1. Local scratch (localStorage) ← user's unsaved drag, always wins
2. User-selected layout source:
├── "Document layout" (_layout) ← authored layout, embedded in the architecture
└── "Saved layout" (API) ← shared team layout, saved via Hub UI
3. Auto-layout (dagre) ← neither exists

**UI Interaction**

- Both sources available: Show a layout source toggle/selector near the existing Save/Reset buttons (e.g. a segmented control or dropdown: "Document layout" | "Saved layout"). The active choice is applied
immediately. Default to whichever was most recently modified, or _layout if timestamps are unavailable.
- Only _layout available (no server-side layout saved): Apply _layout automatically, no selector shown. "Save as default layout" remains available to promote the document layout to the server.
- Only server-side layout available (no _layout in document): Apply server layout automatically, no selector shown. Identical to current behaviour.
- Neither available: Fall through to auto-layout (dagre). Identical to current behaviour.

**Why user choice, not fixed precedence**

- A fixed precedence (e.g. always prefer _layout) removes team agency — a team that deliberately saved a different server layout as the agreed default would have it silently overridden by whatever the document
author embedded.
- Conversely, always preferring the server layout ignores the document author's intent.
- Letting the user choose respects both perspectives and makes the layout source transparent rather than magical.

**Persistence of choice**

- The user's selected layout source preference can be stored in localStorage per (namespace, architectureId) so it persists across navigations and sessions.
- If the selected source becomes unavailable (e.g. _layout is removed from the document in a newer version), fall through gracefully to the other source or auto-layout.

---

### Target Project:
...which existing project in the monorepo will this feature be added to?

### Description of Feature:
...what capabilities will the new feature provide, and why is it needed?

### User Stories:
...describe the feature from an end-user perspective, using "As a [role], I want [feature] so that [benefit]" format...

### Current Limitations:
...describe why this functionality isn't possible with the current implementation...

### Proposed Implementation:
...provide details of the intended implementation approach, including:
- Technical design considerations
- API changes (if applicable)
- Data model changes (if applicable)
- Dependencies on other components

### Alternatives Considered:
...what other approaches were considered and why were they rejected?

### Testing Strategy:
...how will this feature be tested? Include unit, integration, and any other testing considerations...

### Documentation Requirements:
...what documentation will need to be created or updated?

### Implementation Checklist:
- [ ] Design reviewed and approved
- [ ] Implementation completed
- [ ] Tests written and passing
- [ ] Documentation updated
- [ ] Relevant workflows updated (if needed)
- [ ] Performance impact assessed

### Additional Context:
...add any other context, diagrams, mockups, or screenshots about the feature request here...

Contributor guide

Open the contributing guide

Research direction

Start with calm-hub-ui/src/model/layout.ts and the existing useDefaultLayout hook to trace pinsToStoredPositions, storedPositionsToPins, and layout-source precedence. Then inspect LayoutService, MongoLayoutStore, and the VS Code plugin's metadata._layout handling. Done means Hub and the plugin share full node geometry and the UI can choose between document and server-side layouts while preserving the stated fallbacks.

Written by the indexing model from the issue text.

Assessment

Tech stack
mongodb, react, typescript, vscode
Domain
api, backend, database, frontend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.