Skip to content

fix(calm-suite): preserve CALM document-level keys through Studio round-trip#2691

Closed
eddie-knight wants to merge 1 commit into
fix/calmguard-canonical-relationship-typefrom
fix/calmstudio-roundtrip-document-keys
Closed

fix(calm-suite): preserve CALM document-level keys through Studio round-trip#2691
eddie-knight wants to merge 1 commit into
fix/calmguard-canonical-relationship-typefrom
fix/calmstudio-roundtrip-document-keys

Conversation

@eddie-knight

Copy link
Copy Markdown
Contributor

Description

The journey this fixes: You open a CALM document in CALM Studio — one with flows, document-level metadata, adrs, decorators, a $schema, and/or document-level controls. You edit it (even just rearrange a node) and save. The saved file should differ from the original only by your edit.

Today it doesn't. Studio's model store held only { nodes, relationships }, so the moment you open a document every other top-level section is dropped — and on save the file is rebuilt from that two-key model, so $schema, flows, adrs, decorators, document-level metadata, and document-level controls are silently gone. A document authored in the CLI/Hub (or anywhere) loses those sections the instant it passes through Studio, breaking the cross-tool round trip.

After this PR, the full document round-trips:

  • ✅ All document-level keys ($schema, metadata, flows, adrs, decorators, controls) survive open → edit → save.
  • ✅ Every file Studio writes is self-describing — Save, Save As, and Export inject the canonical CALM 1.2 $schema when absent.
  • ✅ Export no longer overwrites existing decorators — it merges the managed AIGF overlay by unique-id.
  • Bonus: Studio's flow overlay now works for loaded documents — flowState reads model.flows, which was always undefined for opened files before.

What changed

  • applyFromJson preserves all document-level keys; applyFromCanvas merges (graph from the canvas, doc-level keys retained), so a canvas edit no longer wipes them.
  • A shared finalizeCalmForWrite(json) used by Save / Save As / Export strips Studio-internal _template scratch and injects the canonical $schema when absent. AIGF governance-decorator generation stays Export-only (no date-stamp churn on Save).
  • exportAsCalm merges the AIGF decorator by unique-id instead of overwriting the array.

ℹ️ Components note: the change is in CALM Studio (calm-suite/calm-studio/), which has no checkbox in the template's "Affected Components", so none are ticked.

🔗 Stacked PR: targets fix/calmguard-canonical-relationship-type (the CALMGuard fix) rather than main — re-point to main once that merges. (#1 is a different product; this PR is logically independent but chained per the stack.)

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature
  • 💥 Breaking change
  • 📚 Documentation update
  • 🎨 Code style/formatting changes
  • ♻️ Refactoring
  • ⚡ Performance improvements
  • ✅ Test additions or updates
  • 🔧 Chore

Affected Components

(All unchecked — surface is CALM Studio (calm-suite/calm-studio/), no template checkbox.)

Commit Message Format ✅

Conventional Commits, DCO signed-off:

  • fix(calm-suite): preserve CALM document-level keys through Studio round-trip

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 (all pass, incl. new preservation/finalize/decorator-merge coverage), npm run typecheck (no new errors vs the pre-existing baseline), and npm run build. New tests cover preservation of $schema/flows/metadata/adrs/document-level controls/decorators through applyFromJson and a subsequent applyFromCanvas; the flow-overlay regression guard; the full getModelJson → finalizeCalmForWrite → reparse Save path; finalizeCalmForWrite ($schema inject/preserve, _template strip); and decorator merge keep-path + re-export idempotency. Each guard was checked to fail on pre-fix code.

Checklist

  • Conventional commit format
  • Documentation updated if necessary
  • Tests added
  • Follows coding standards

…nd-trip

CALM Studio's model store held only { nodes, relationships }, so opening any
CALM document and saving silently dropped every other top-level section
($schema, document-level metadata, flows, adrs, decorators, document-level
controls). This broke the cross-tool round trip — a doc authored in the CLI/Hub
or any tool lost its flows/decorators/schema the moment it passed through Studio.

- applyFromJson now preserves all document-level keys; applyFromCanvas merges
  (graph from the canvas, doc-level keys retained) so a canvas edit no longer
  wipes them.
- Add a shared finalizeCalmForWrite(json) used by Save, Save As, and Export:
  strips Studio-internal _template scratch and injects the canonical CALM 1.2
  $schema when absent, so every written file is self-describing. AIGF governance
  decorator generation stays Export-only (no date-stamp churn on Save).
- exportAsCalm now merges the AIGF decorator by unique-id instead of overwriting
  the decorators array, preserving any existing decorators.
- Also fixes Studio's flow overlay for loaded documents: flowState reads
  model.flows, which was always undefined for opened files.

Tests: store-level preservation through applyFromJson + applyFromCanvas for
$schema, flows, metadata, adrs, document-level controls, and decorators; the
flow-overlay regression guard; a full Save-path composition (getModelJson ->
finalizeCalmForWrite -> reparse); finalizeCalmForWrite ($schema inject/preserve,
_template strip); and decorator merge keep-path + re-export idempotency.

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

eddie-knight commented Jun 19, 2026

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: #2683

@gjs-opsflo gjs-opsflo 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.

Clean, surgical, well-scoped. Tests directly pin every guarantee the PR body makes.

Strengths

  • AIGF kept Export-only. This was the most failure-prone refactor risk and the explicit comment + does NOT add a governance decorator (AIGF is Export-only) test guards it. The AIGF merge by unique-id (filtering aigf-governance-overlay) is also a quiet upgrade — idempotent re-export, no overwrite of user decorators.
  • $schema injection gated on absence. A non-canonical existing $schema is preserved. Tested via preserves an existing $schema untouched.
  • finalizeCalmForWrite is pure + idempotent + handles malformed JSON. Right shape for a shared writer step.
  • All save paths covered. Verified handleSave, handleSaveAs, Cmd+S / Cmd+Shift+S, Tauri writeTextFile route through the wrapped handlers. No autosave or direct-write bypass.
  • Tests assert the contract, not the implementation. Inject-when-absent, preserve-when-present, _template strip, AIGF-not-on-Save, malformed-JSON passthrough. Integration block walks the full applyFromJson → calmToFlow → applyFromCanvas round-trip for every doc-level section.

Minor follow-up suggestions (non-blocking)

  1. Nested doc-level keys shared by reference in model = { ...model, nodes: [...], relationships: [...] }. metadata / flows / decorators / adrs / document-level controls carry through by reference. Confirmed safe today (no in-place mutator exists), but worth a code comment so a future "edit metadata" UI doesn't introduce a stealth reactivity bug.
  2. $schema not in CalmArchitecture type. Currently lives in & { $schema?: string } casts. Would be cleaner to extend the canonical type in @calmstudio/calm-core so callers don't keep re-casting. Trivial follow-up.
  3. Calmscript export bypasses finalize — correct (text artifact, not CALM JSON), but worth a one-line comment so a future reader doesn't "fix" it.

LGTM as-is.

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