finos / finos/architecture-as-code
RFC: shared CALM semantics layer to align calm-hub-ui and calm-studio
- Dominant language
- TypeScript
- Stars
- 399
- Forks
- 138
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 38
Description
## 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
1. **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.
2. **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.
3. **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.
4. **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?
5. **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
- [ ] Boundary agreed (semantic layer = shared, UI = per-app).
- [ ] Decision on package home (new top-level vs `@finos/calm-models` sub-export).
- [ ] DRI / CODEOWNERS scope identified.
- [ ] Phase 1 issue or PR opened with the extraction plan above.
## Related
- Concurrent viz revamp spike in calm-studio: #2686. That spike's `viz/` pure modules in `calm-suite/calm-studio/packages/calm-core/src/viz/` are structured to be relocatable to `@finos/calm-viz-core` as a rename + re-export, not a rewrite.
Contributor guide
Research direction
Start by reviewing the proposed boundary and acceptance criteria in this RFC, then compare calm-studio/packages/calm-core/src/helpers.ts with calm-hub-ui/src/visualizer/components/reactflow/relationshipParser.ts and nodeParser.ts. Review the related viz modules in issue #2686. Done means the semantic-versus-UI boundary, package home, ownership scope, and Phase 1 follow-up issue or PR are agreed.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend, tooling
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100