Skip to content

fix(calm-suite): round-trip multi-child, interface, and options relationships in Studio#2692

Draft
eddie-knight wants to merge 2 commits into
fix/calmstudio-roundtrip-document-keysfrom
fix/calmstudio-flowtocalm-graph-fidelity
Draft

fix(calm-suite): round-trip multi-child, interface, and options relationships in Studio#2692
eddie-knight wants to merge 2 commits into
fix/calmstudio-roundtrip-document-keysfrom
fix/calmstudio-flowtocalm-graph-fidelity

Conversation

@eddie-knight

@eddie-knight eddie-knight commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Description

The journey this fixes: You open a CALM architecture in CALM Studio — one with a container that holds several children (a composed-of/deployed-in with many nodes), some connects relationships that bind specific interfaces, and maybe an options decision. You rearrange the nodes on the canvas and save. The saved file should be structurally identical to what you loaded — only positions changed.

Today it isn't. Studio's CALM↔canvas projection (projection.ts) silently degrades the graph on that round-trip:

  • Multi-child relationships fragment. A single composed-of with 3 children comes back as three separate single-child relationships with mangled ids (box-children#0, #1, #2) — destroying the original unique-id that flows and options decisions reference by id.
  • connects interface bindings are dropped. { node, interfaces }{ node }, so endpoint interface references are lost.
  • options relationships vanish or go invalid. Loaded decisions are dropped entirely on a canvas edit, and the "Options" edge-type could only ever emit schema-invalid { options: [] } (Studio has no UI to author a decision's description/nodes/relationships).

The net effect: any tool downstream of Studio (the CLI validator, the Hub, flow overlays, generators) sees a structurally degraded document. This is the graph-payload counterpart to the document-level-key loss fixed in the parent PR.

After this PR, the round-trip is lossless:

  • ✅ Multi-child composed-of/deployed-in/interacts relationships re-aggregate into one relationship with the original unique-id and all children intact.
  • connects endpoint interfaces survive the round-trip.
  • options relationships loaded from a file are preserved, with referential GC (a decision whose referenced node/relationship was deleted on the canvas is dropped rather than left dangling).

What changed

  • calmToFlow now carries connects endpoint interfaces on the edge; flowToCalm re-aggregates edges by their source-relationship id (calmRelId), restoring the original unique-id and merging multi-child targets into one relationship. buildRelationshipType restores interfaces and throws on options (it has no edge form) instead of emitting invalid CALM.
  • applyFromCanvas preserves loaded options relationships from the prior model with referential GC, since options have no edge representation.
  • EdgeProperties edits resolve the relationship via calmRelId, so editing a multi-child edge updates its (aggregated) relationship instead of silently no-opping; the read-only Unique ID field shows the relationship id.

⚠️ User-visible behavior change (intentional, reviewed): the "Options" edge-type affordance is removed from the relationship-type dropdown and the canvas context menu. Studio has no decision-authoring UI, so that affordance could only produce schema-invalid CALM. Options relationships loaded from a file still round-trip — this only removes the ability to create one on the canvas, which never worked correctly. A real decision-authoring UI would be a separate feature.

🔗 Stacked PR: targets fix/calmstudio-roundtrip-document-keys (the document-level-keys fix) rather than main, so the diff is scoped to this change. Re-point to main once the parent merges.

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🎨 Code style/formatting changes
  • ♻️ Refactoring (no functional changes)
  • ⚡ Performance improvements
  • ✅ Test additions or updates
  • 🔧 Chore (maintenance, dependencies, CI, etc.)

Affected Components

  • CLI (cli/)
  • Schema (calm/)
  • CALM AI (calm-ai/)
  • CALM Hub (calm-hub/)
  • CALM Hub UI (calm-hub-ui/)
  • CALM Server (calm-server/)
  • CALM Widgets (calm-widgets/)
  • Documentation (docs/)
  • Shared (shared/)
  • VS Code Extension (calm-plugins/vscode/)
  • Dependencies
  • CI/CD

Commit Message Format ✅

Conventional Commits, DCO signed-off:

  • fix(calm-suite): round-trip multi-child, interface, and options relationships in Studio

Testing

  • I have tested my changes locally
  • I have added/updated unit tests
  • All existing tests pass

Verified on Node 22: npm run test --workspace=@calmstudio/studio (457 pass, incl. new fidelity/options coverage), npm run typecheck --workspace=@calmstudio/studio (no new errors vs the pre-existing baseline), and npm run build --workspace=@calmstudio/studio all pass. New tests cover: connects-interface round-trip; multi-child re-aggregation for composed-of/deployed-in/interacts (set-equality of nodes, original unique-id, container/actor preserved); the full applyFromJson → calmToFlow → applyFromCanvas wiring path; a drawn edge staying its own relationship (not absorbed into a group); options preservation; decision GC on a deleted reference (including partial-survival and empty-ref-kept); flowToCalm never emitting options; and editing a multi-child edge by calmRelId. Each fidelity guard was checked to fail on pre-fix code.

Checklist

  • My commits follow the conventional commit format
  • I have updated documentation if necessary
  • I have added tests for my changes (if applicable)
  • My changes follow the project's coding standards

…ionships in Studio

Studio's CALM<->canvas projection lost graph fidelity on a load->edit->save
round-trip: connects endpoint interfaces were dropped, multi-child
composed-of/deployed-in/interacts relationships fragmented into N single-child
relationships with mangled `#i` ids (destroying the original unique-id that
flows and options decisions reference), and options relationships vanished or
were emitted as schema-invalid `{ options: [] }`.

- calmToFlow now carries connects endpoint interfaces on the edge; flowToCalm
  re-aggregates edges by their source-relationship id (`calmRelId`), restoring
  the original unique-id and merging multi-child targets into one relationship.
  buildRelationshipType restores interfaces and throws on `options` (it has no
  edge form) instead of emitting invalid CALM.
- applyFromCanvas preserves loaded options relationships from the prior model
  with referential GC (drops decisions whose node/relationship refs no longer
  resolve), since options have no edge representation.
- Remove the non-functional "Options" edge-type affordance (EdgeProperties
  dropdown + CalmCanvas context menu): Studio has no decision-authoring UI, so
  it could only ever produce invalid CALM. Loaded options still round-trip.
- EdgeProperties edits resolve the relationship via `calmRelId` so editing a
  multi-child edge updates its (aggregated) relationship instead of no-opping.

Tests: connects-interface round-trip; multi-child re-aggregation for
composed-of/deployed-in/interacts (set-equality, original id); new-edge
handling; options preservation + decision GC on node deletion; flowToCalm never
emitting options; and multi-child edit-by-calmRelId.

Signed-off-by: Eddie Knight <knight@linux.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes CALM Studio’s canvas ↔ CALM projection round-trip fidelity so that multi-child relationships, connects interface bindings, and file-loaded options decisions aren’t structurally degraded when users edit node positions and save.

Changes:

  • Re-aggregate multi-child composed-of/deployed-in/interacts edges back into a single CALM relationship keyed by the original unique-id (calmRelId), and preserve connects endpoint interface bindings.
  • Preserve file-loaded options relationships across canvas edits via applyFromCanvas, with referential GC of invalid decision references; remove “Options” as an authorable edge type in the UI.
  • Add/extend integration and component tests covering interface round-trip, multi-child re-aggregation, options preservation/GC, and editing by calmRelId.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
calm-suite/calm-studio/apps/studio/src/tests/integration/sync-integration.test.ts Adds integration coverage for lossless round-trips (interfaces, multi-child re-aggregation, options preservation/GC).
calm-suite/calm-studio/apps/studio/src/tests/components/EdgeProperties.test.ts Updates EdgeProperties tests for “no options edge type” and calmRelId-based editing behavior.
calm-suite/calm-studio/apps/studio/src/lib/stores/projection.ts Updates CALM↔Flow projection to carry interface bindings and re-aggregate edges by calmRelId.
calm-suite/calm-studio/apps/studio/src/lib/stores/calmModel.svelte.ts Preserves options relationships across canvas edits and performs referential GC.
calm-suite/calm-studio/apps/studio/src/lib/properties/EdgeProperties.svelte Removes options from authorable relationship types; targets edits via calmRelId and displays relationship id.
calm-suite/calm-studio/apps/studio/src/lib/canvas/CalmCanvas.svelte Removes “Options” from the edge-type context menu options.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread calm-suite/calm-studio/apps/studio/src/lib/stores/calmModel.svelte.ts Outdated
Comment thread calm-suite/calm-studio/apps/studio/src/lib/stores/projection.ts
…lity

Three fixes from PR #2692 review:

- EdgeProperties: changing the relationship type of a multi-child relationship
  rebuilt it from only the edited edge's endpoints, silently dropping the other
  children. Rebuild from the relationship's full membership (read from the model
  by calmRelId) so composed-of/deployed-in/interacts conversions preserve every
  child; connects (1:1) reduces to the first member.
- applyFromCanvas: stop dropping an options relationship whose decisions array
  ends up empty. `option-type` has no minItems, so `options: []` is valid CALM;
  dropping it was lossy for a legitimately-empty input. Referential GC of
  dangling decisions is kept, but the relationship itself is preserved.
- projection.ts: update the stale flowToCalm docstring that still described
  multi-child round-trips as a lossy split into single-child relationships.

Tests: editing a multi-child edge's type now asserts all children are preserved;
a legitimately-empty `options: []` survives a round-trip; the GC test asserts the
relationship is kept (decisions GC'd) rather than dropped.

Signed-off-by: Eddie Knight <knight@linux.com>
@eddie-knight eddie-knight marked this pull request as draft June 19, 2026 14:21
@eddie-knight

Copy link
Copy Markdown
Contributor Author

My currently open PRs are stacked to avoid any merge conflicts or rebasing overhead. This can be taken out of draft when the preceding PR is merged: #2691

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants