Context
CALM 1.2 is rendered today by two web surfaces in this repo:
- calm-hub-ui — mature read-only architecture viewer (React 19 + ReactFlow + Dagre, ~6,700 LOC).
- calm-studio — early interactive editor (Svelte 5 + Svelte Flow + ELK.js), plus the framework-agnostic
@calmstudio/diagram web component.
Both consume the same canonical model (@finos/calm-models, CALM 1.2). Both independently re-derive the same semantic interpretations of that model — relationship-variant routing, container detection, node-type metadata, decorator parsing. Concretely:
- Relationship-variant traversal duplicated.
@calmstudio/calm-core/src/helpers.ts exposes getRelationshipVariant(), getConnectsEndpoints(), getContainerAndNodes(), getActorAndNodes(), getReferencedNodeIds(). calm-hub-ui hand-rolls the same rel['relationship-type']?.connects / .deployed-in / .composed-of / .interacts routing in calm-hub-ui/src/visualizer/components/reactflow/relationshipParser.ts and nodeParser.ts.
- Node-type → color/icon metadata duplicated. calm-studio derives it from
@calmstudio/extensions pack registry + CSS variables; calm-hub-ui hard-codes it in calm-hub-ui/src/visualizer/components/reactflow/theme.ts and CustomNode.tsx.
- Decorator interpretation rules will diverge as soon as either app starts rendering decorator-driven semantics (e.g. threats, controls coverage, ownership tints). Today neither has full decorator coverage — the divergence risk is prospective, before either side hardens its interpretation.
This is not a line-count problem. It is a correctness-drift problem at the canonical-semantics layer. When the CALM 1.2 spec evolves — new variant on a relationship type, new decorator pattern, refined node-type taxonomy — each application has to independently re-implement the new interpretation. Silent divergence is the failure mode.
There is also a user-facing coherence problem. Architects using both tools see the same CALM document rendered with different node colors, different relationship semantics for composed-of (rendered as an arrow in one, as nested containment in the other), and different decorator surfacing. This creates a fragmented impression of what CALM "is".
Goal
Establish a single source of truth for CALM 1.2 semantic interpretation, consumed by both calm-hub-ui and calm-studio. Eliminate the correctness-drift risk. Improve user-facing coherence of the visual language across CALM web surfaces.
Constraint
UI components are framework-bound. React components cannot natively render inside a Svelte 5 application, and vice versa. Therefore:
- A shared React component library can serve calm-hub-ui (and any future React surface), but cannot be consumed by calm-studio directly without web-component wrappers (with associated runtime + bundle costs).
- A shared Svelte 5 component library can serve calm-studio but cannot be consumed by calm-hub-ui directly.
- The only artifact that crosses the framework boundary cleanly is framework-agnostic TypeScript — pure data accessors, types, and prop contracts.
Both products are valid in their respective domains. calm-hub-ui is a polished viewer with rich detail panels; calm-studio is an editor whose interaction model genuinely differs from a viewer (add/remove fields, inline edit affordances, validation surfacing during edit, undo/redo integration). Forcing one to render the other's components would either (a) ship a second runtime in the smaller app's bundle, or (b) break the editing affordances that justify the editor's existence.
Alternatives considered
Option 1 — Do nothing, accept duplication
Each app independently maintains its CALM 1.2 interpretation. Per-spec-evolution coordination tax. Silent drift over time.
Option 2 — Migrate calm-studio to React
Rewrite 8 calm-studio packages + VSCode extension in React. Loses @calmstudio/diagram's framework-agnostic value. Reintroduces well-known React-in-editor pitfalls (stale closures, batching, effect deps). Multi-quarter delay on actual feature work. Classic rewrite trap.
Option 3 — Migrate calm-hub-ui to Svelte 5
Symmetric to Option 2, applied to the larger mature codebase. Worse cost-benefit.
Option 4 — Extract Hub UI React components into @finos/calm-ui, calm-studio consumes via web-component wrappers
Ships React runtime in calm-studio's bundle (~45KB+ gzipped for ReactDOM alone). Shadow DOM boundary breaks design tokens. Hub UI components are read-only; editor mode would have to layer on top via custom-element messaging. Freezes read-only assumptions into an editor's foundation.
Option 5 — Extract a framework-agnostic CALM semantics layer (data + contracts), keep UI per-framework
Publish @finos/calm-viz-core: pure TypeScript, no UI framework dependency. Contains relationship-variant accessors, node-type metadata, decorator parsing, severity-resolution helpers, prop-contract types. Both apps consume immediately. calm-hub-ui writes React components against the shared contracts. calm-studio writes Svelte components against the same contracts. Visual languages can diverge intentionally where the editor context demands it, but the semantic interpretation is single-source-of-truth.
Option 6 — Extract @finos/calm-ui-react for shared React components
Additionally (or instead of Option 5): lift ControlCard, FlowCard, NodeDetails, Deployment StatusBadge, AdrsPanel from calm-hub-ui into a shared React package. Useful for future React surfaces (calm-cli docs site, third-party tooling). Does not help calm-studio directly without web-component wrappers (Option 4 territory). Without a second React consumer at extraction time, the refactor PR has the shape of a no-behavior-change inventory PR — historically drawn legitimate maintainer pushback in this repo.
Recommendation
Phase 0 — Alignment (this issue). Agree on the boundary between shared and per-app. Confirm that the right unit of sharing is semantic interpretation, not visual implementation. Confirm that framework parity is a non-goal.
Phase 1 — Data-layer extraction (Option 5). Scaffold @finos/calm-viz-core as a new workspace package. Pure TypeScript, no framework dependencies. Land in a single PR that:
- Promotes
helpers.ts accessors from @calmstudio/calm-core to @finos/calm-viz-core.
- Adds node-type metadata table.
- Adds decorator parsing + severity-resolution helpers.
- Adds prop-contract types for shared presentational concepts (types only).
- Refactors calm-hub-ui's
relationshipParser.ts + nodeParser.ts to consume the shared accessors; deletes the hand-rolled copies.
- Has
@calmstudio/calm-core re-export from @finos/calm-viz-core for zero-disruption migration.
- Ships with tests.
Visible diff: calm-hub-ui shrinks ~300 LOC of duplicated logic. New package appears. Two consumers wired in same PR. Avoids the no-diff inventory-PR shape.
The parallel calm-studio viz revamp spike (issue #2686) has already structured its new pure modules in calm-suite/calm-studio/packages/calm-core/src/viz/ as framework-agnostic TypeScript with no Svelte runes. Those modules — BadgeAPI, decoratorsAdapter, controlsAdapter, severityResolver, grouping dimensions + groupingEngine, positionStore — are candidate first-draft input for @finos/calm-viz-core once its boundary is agreed here.
Phase 2 — Improvements within each app, against shared contracts (parallel, optional). With shared semantics in place, calm-hub-ui and calm-studio independently improve their UIs. Each app stays in its native framework. New features that touch CALM semantics route through @finos/calm-viz-core.
Phase 3 — Shared React component package (Option 6), only if a second React consumer materializes. If calm-cli grows a web docs renderer, or a third-party tool wants to embed CALM panels, then @finos/calm-ui-react becomes justified — extract one component per PR, each driven by an actual consumer beyond calm-hub-ui itself.
Phase 4 — Long-term watch. Revisit framework decisions only with concrete evidence of ecosystem fragmentation impacting users or contributors. Speculative framework migration is not on the table.
Non-goals
- Framework migration in either direction. Both calm-hub-ui (React 19) and calm-studio (Svelte 5) remain in their current stacks.
- Shared canvas engine. ReactFlow and Svelte Flow are inherently per-framework; Dagre and ELK have meaningfully different layout characteristics neither app should be forced to abandon. The canvas is the right place for divergence.
- Shared design system / theme tokens. Out of scope for this RFC. Could be a parallel effort via CSS variables; not required for semantic alignment.
- Forcing calm-studio adoption of Hub UI's React components. Studio writes Svelte UI against shared contracts.
Open questions for community
- Package home. Should
@finos/calm-viz-core be a new top-level workspace, or live under @finos/calm-models as a sub-export? Argument for new package: keeps calm-models pure types. Argument for sub-export: fewer packages to publish/version.
- Ownership model. Cross-cutting packages need a DRI to avoid stalling on cross-team review. Who owns
@finos/calm-viz-core? Proposal: shared between current calm-hub-ui and calm-studio maintainers, with explicit CODEOWNERS entry.
- CSS strategy for any future
@finos/calm-ui-react. calm-hub-ui uses Tailwind + DaisyUI. A shared React package would need to either (a) ship Tailwind classes assuming consumer has Tailwind, (b) ship CSS Modules, or (c) ship raw scoped CSS. Out of scope for Phase 1.
- Decorator schema validation. calm-cli does not currently enforce
decorators[] at validate-time. This is why some CALM documents in circulation today encode threats in metadata.threat-model.* rather than canonical decorators[]. Decorator schema enforcement is a likely sibling phase to this work — should it be tracked here or as a separate issue?
- Two-issue split. This RFC scopes the alignment effort. Should Phase 1 (the actual
@finos/calm-viz-core extraction PR) live as its own issue once boundary agreement is reached, or proceed directly from this thread?
Acceptance criteria for closing this RFC
Related
Context
CALM 1.2 is rendered today by two web surfaces in this repo:
@calmstudio/diagramweb component.Both consume the same canonical model (
@finos/calm-models, CALM 1.2). Both independently re-derive the same semantic interpretations of that model — relationship-variant routing, container detection, node-type metadata, decorator parsing. Concretely:@calmstudio/calm-core/src/helpers.tsexposesgetRelationshipVariant(),getConnectsEndpoints(),getContainerAndNodes(),getActorAndNodes(),getReferencedNodeIds(). calm-hub-ui hand-rolls the samerel['relationship-type']?.connects / .deployed-in / .composed-of / .interactsrouting incalm-hub-ui/src/visualizer/components/reactflow/relationshipParser.tsandnodeParser.ts.@calmstudio/extensionspack registry + CSS variables; calm-hub-ui hard-codes it incalm-hub-ui/src/visualizer/components/reactflow/theme.tsandCustomNode.tsx.This is not a line-count problem. It is a correctness-drift problem at the canonical-semantics layer. When the CALM 1.2 spec evolves — new variant on a relationship type, new decorator pattern, refined node-type taxonomy — each application has to independently re-implement the new interpretation. Silent divergence is the failure mode.
There is also a user-facing coherence problem. Architects using both tools see the same CALM document rendered with different node colors, different relationship semantics for
composed-of(rendered as an arrow in one, as nested containment in the other), and different decorator surfacing. This creates a fragmented impression of what CALM "is".Goal
Establish a single source of truth for CALM 1.2 semantic interpretation, consumed by both calm-hub-ui and calm-studio. Eliminate the correctness-drift risk. Improve user-facing coherence of the visual language across CALM web surfaces.
Constraint
UI components are framework-bound. React components cannot natively render inside a Svelte 5 application, and vice versa. Therefore:
Both products are valid in their respective domains. calm-hub-ui is a polished viewer with rich detail panels; calm-studio is an editor whose interaction model genuinely differs from a viewer (add/remove fields, inline edit affordances, validation surfacing during edit, undo/redo integration). Forcing one to render the other's components would either (a) ship a second runtime in the smaller app's bundle, or (b) break the editing affordances that justify the editor's existence.
Alternatives considered
Option 1 — Do nothing, accept duplication
Each app independently maintains its CALM 1.2 interpretation. Per-spec-evolution coordination tax. Silent drift over time.
Option 2 — Migrate calm-studio to React
Rewrite 8 calm-studio packages + VSCode extension in React. Loses
@calmstudio/diagram's framework-agnostic value. Reintroduces well-known React-in-editor pitfalls (stale closures, batching, effect deps). Multi-quarter delay on actual feature work. Classic rewrite trap.Option 3 — Migrate calm-hub-ui to Svelte 5
Symmetric to Option 2, applied to the larger mature codebase. Worse cost-benefit.
Option 4 — Extract Hub UI React components into
@finos/calm-ui, calm-studio consumes via web-component wrappersShips React runtime in calm-studio's bundle (~45KB+ gzipped for ReactDOM alone). Shadow DOM boundary breaks design tokens. Hub UI components are read-only; editor mode would have to layer on top via custom-element messaging. Freezes read-only assumptions into an editor's foundation.
Option 5 — Extract a framework-agnostic CALM semantics layer (data + contracts), keep UI per-framework
Publish
@finos/calm-viz-core: pure TypeScript, no UI framework dependency. Contains relationship-variant accessors, node-type metadata, decorator parsing, severity-resolution helpers, prop-contract types. Both apps consume immediately. calm-hub-ui writes React components against the shared contracts. calm-studio writes Svelte components against the same contracts. Visual languages can diverge intentionally where the editor context demands it, but the semantic interpretation is single-source-of-truth.Option 6 — Extract
@finos/calm-ui-reactfor shared React componentsAdditionally (or instead of Option 5): lift
ControlCard,FlowCard,NodeDetails,Deployment StatusBadge,AdrsPanelfrom calm-hub-ui into a shared React package. Useful for future React surfaces (calm-cli docs site, third-party tooling). Does not help calm-studio directly without web-component wrappers (Option 4 territory). Without a second React consumer at extraction time, the refactor PR has the shape of a no-behavior-change inventory PR — historically drawn legitimate maintainer pushback in this repo.Recommendation
Phase 0 — Alignment (this issue). Agree on the boundary between shared and per-app. Confirm that the right unit of sharing is semantic interpretation, not visual implementation. Confirm that framework parity is a non-goal.
Phase 1 — Data-layer extraction (Option 5). Scaffold
@finos/calm-viz-coreas a new workspace package. Pure TypeScript, no framework dependencies. Land in a single PR that:helpers.tsaccessors from@calmstudio/calm-coreto@finos/calm-viz-core.relationshipParser.ts+nodeParser.tsto consume the shared accessors; deletes the hand-rolled copies.@calmstudio/calm-corere-export from@finos/calm-viz-corefor zero-disruption migration.Visible diff: calm-hub-ui shrinks ~300 LOC of duplicated logic. New package appears. Two consumers wired in same PR. Avoids the no-diff inventory-PR shape.
The parallel calm-studio viz revamp spike (issue #2686) has already structured its new pure modules in
calm-suite/calm-studio/packages/calm-core/src/viz/as framework-agnostic TypeScript with no Svelte runes. Those modules —BadgeAPI,decoratorsAdapter,controlsAdapter,severityResolver, groupingdimensions+groupingEngine,positionStore— are candidate first-draft input for@finos/calm-viz-coreonce its boundary is agreed here.Phase 2 — Improvements within each app, against shared contracts (parallel, optional). With shared semantics in place, calm-hub-ui and calm-studio independently improve their UIs. Each app stays in its native framework. New features that touch CALM semantics route through
@finos/calm-viz-core.Phase 3 — Shared React component package (Option 6), only if a second React consumer materializes. If calm-cli grows a web docs renderer, or a third-party tool wants to embed CALM panels, then
@finos/calm-ui-reactbecomes justified — extract one component per PR, each driven by an actual consumer beyond calm-hub-ui itself.Phase 4 — Long-term watch. Revisit framework decisions only with concrete evidence of ecosystem fragmentation impacting users or contributors. Speculative framework migration is not on the table.
Non-goals
Open questions for community
@finos/calm-viz-corebe a new top-level workspace, or live under@finos/calm-modelsas a sub-export? Argument for new package: keeps calm-models pure types. Argument for sub-export: fewer packages to publish/version.@finos/calm-viz-core? Proposal: shared between current calm-hub-ui and calm-studio maintainers, with explicit CODEOWNERS entry.@finos/calm-ui-react. calm-hub-ui uses Tailwind + DaisyUI. A shared React package would need to either (a) ship Tailwind classes assuming consumer has Tailwind, (b) ship CSS Modules, or (c) ship raw scoped CSS. Out of scope for Phase 1.decorators[]at validate-time. This is why some CALM documents in circulation today encode threats inmetadata.threat-model.*rather than canonicaldecorators[]. Decorator schema enforcement is a likely sibling phase to this work — should it be tracked here or as a separate issue?@finos/calm-viz-coreextraction PR) live as its own issue once boundary agreement is reached, or proceed directly from this thread?Acceptance criteria for closing this RFC
@finos/calm-modelssub-export).Related
viz/pure modules incalm-suite/calm-studio/packages/calm-core/src/viz/are structured to be relocatable to@finos/calm-viz-coreas a rename + re-export, not a rewrite.