Skip to content

fix(calm-suite): adopt canonical nested relationship-type in CALMGuard for cross-tool interop#2683

Closed
eddie-knight wants to merge 4 commits into
mainfrom
fix/calmguard-canonical-relationship-type
Closed

fix(calm-suite): adopt canonical nested relationship-type in CALMGuard for cross-tool interop#2683
eddie-knight wants to merge 4 commits into
mainfrom
fix/calmguard-canonical-relationship-type

Conversation

@eddie-knight

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

Copy link
Copy Markdown
Contributor

Description

The journey this fixes: You model an architecture in CALM Studio, export it (or grab any CALM document the CLI / Hub produces), then open CALMGuard to run compliance analysis on it. Today that fails — the document is rejected the moment you upload it, before any analysis runs.

Concretely: take Studio's own ecommerce demo export and drop it into CALMGuard's upload zone → it's refused at the CALM-schema step.

The cause is a dialect mismatch. Canonical CALM 1.2 nests the relationship variant inside an object:

"relationship-type": { "connects": { "source": { "node": "a" }, "destination": { "node": "b" } } }

…but CALMGuard hand-rolled its own Zod schema using a flat string discriminant with a sibling key ("relationship-type": "connects" + a sibling "connects": {…}). So CALMGuard was the only tool in the suite speaking a private dialect: it couldn't read what Studio/CLI/Hub/Visualizer produce, and the remediation PRs it wrote back were schema-invalid to those same tools. It also enforced a closed 9-value node-type enum, so Studio extension node-types (e.g. aws:lambda) were rejected too.

After this PR, the round trip works:

  • ✅ A canonical CALM document from Studio / CLI / Hub uploads and analyzes in CALMGuard.
  • ✅ CALMGuard's remediation PR output is canonical CALM that calm validate, the Hub, and the Visualizer accept.
  • ✅ Extension node-type values from Studio packs are accepted.

What changed

  • Rewrote CALMGuard's relationship schema to the canonical nested relationship-type object (exactly-one-variant enforced via superRefine) and relaxed node-type to an open string, matching the CALM core spec.
  • Added getRelationshipVariant() and routed every reader (CALM extractor, the React-Flow graph mapping, the learning extractor) through the nested shape.
  • Made the v1.0 normalizer emit the nested form.
  • Added a compile-time conformance check against @finos/calm-models types (gated by tsc, so drift fails the typecheck) plus interop tests that parse real canonical fixtures authored by other CALM tools and assert the legacy flat form is now rejected.
  • Converted CALMGuard's demo examples and inline test fixtures to nested; updated calm-guard/AGENTS.md.

This PR also includes a small unrelated convenience commit, chore(calm-suite): add Makefile to launch suite apps on demand — happy to drop it if you'd prefer the PR scoped purely to the fix.

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): adopt canonical nested relationship-type in CALMGuard for cross-tool interop
  • chore(calm-suite): add Makefile to launch suite apps on demand

Testing

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

Verified on Node 22: npm run typecheck, npm run lint, npm run test:run (117 tests, including new interop + conformance coverage), and npm run build for calmguard all pass. Manually confirmed against a real Studio export: pre-fix CALMGuard rejects it at the upload zone; post-fix it parses and proceeds to analysis.

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

…d for cross-tool interop

CALMGuard modeled `relationship-type` as a flat string discriminant with a
sibling variant key, which is not canonical CALM 1.2. As a result it rejected
nested documents authored by CALM Studio / the CLI / Hub at parse time, and its
own PR write-backs were schema-invalid to those tools.

- Rewrite the Zod schema to the canonical nested `relationship-type` object,
  enforcing exactly-one-variant via superRefine, and relax `node-type` to an
  open string so extension node-types (e.g. aws:lambda) interoperate.
- Add getRelationshipVariant() and route all readers (calm extractor,
  calm-to-flow graph mapping, learning extractor) through the nested shape.
- Emit the nested form from the v1.0 normalizer.
- Add a compile-time conformance check against @finos/calm-models types
  (gated by tsc, not vitest), plus interop tests that parse real canonical
  fixtures authored by other CALM tools and reject the legacy flat form.
- Convert demo examples and inline test fixtures to nested; update AGENTS.md.

Adds @finos/calm-models as a direct dependency of calmguard.

Signed-off-by: Eddie Knight <knight@linux.com>
Slim launcher wrapping the existing npm workspace scripts: guard / guard-docs /
studio / studio-desktop dev servers, builds, and tests. Runs from the repo root
and builds @calmstudio/calm-core before starting Studio (it ships from dist).

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

CALMGuard's compile-time conformance check (src/lib/calm/conformance.ts) imports
types from @finos/calm-models. That package ships from dist/ and has no prepare
script, so a clean `npm ci` does not build it — the Lint/Typecheck and Build
jobs then fail to resolve `@finos/calm-models/types` (TS2307). Build it first in
both jobs, matching how the calm-studio workflow already sequences it.

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

Copy link
Copy Markdown
Member

Thanks @eddie-knight — this stack (#2683, #2691, #2692, #2693) is squarely in the direction of the convergence epic #2600; canonical CALM interop across the suite is exactly the end-state we're aiming for.

My one concern before merging is sequence. Each PR overlaps a track that's already scoped as an issue:

The fixes are real, and some of the work is a genuine down-payment on the tracks (e.g. the @finos/calm-models conformance check in #2683). But addressing them out of sequence, with no link back to the issues, makes the work hard to see against the plan and risks duplicate or throwaway effort once the tracks start.

Two asks:

  1. Please reference the relevant track from each PRfix(calm-suite): adopt canonical nested relationship-type in CALMGuard for cross-tool interop #2683/fix(calm-suite): validate remediated CALM before opening a CALMGuard PR #2693[Track B] Capability convergence — validators, MCP, VS Code extension #2650, fix(calm-suite): preserve CALM document-level keys through Studio round-trip #2691/fix(calm-suite): round-trip multi-child, interface, and options relationships in Studio #2692[Track C] CALM Studio as a desktop editor over the platform #2651, all → A3 in [Track A] Structural cleanup of calm-suite (no behaviour change) #2649 — so the overlap is visible.

  2. @gjs-opsflo, you put the track breakdown together, so I'd like your call on sequencing:

    • (a) merge these now and fold the superseded parts (Zod model, CLI shell-out) into B1 when it starts; or
    • (b) hold the stack and address the issues in the requested order (A → B → C) for clarity.

    Either works for me — I'd just like us aligned before these merge.

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

Strong PR — schema migration is clean and the compile-time conformance.ts gate vs @finos/calm-models is a great move. New interop.test.ts covering real cross-tool fixtures (complex.architecture.json, all 4 concrete variants) is the right anchor test. Normalizer leniency on input + strict canonical output is exactly the contract we need for round-trip with Studio and the CLI.

Requesting one change before merge

calm-suite/calm-guard/package.json:45"@finos/calm-models": "file:../../calm-models"

calm-models is already a root npm workspace, so this should use the workspace protocol so the package resolves via the workspace symlink rather than a relative file path:

"@finos/calm-models": "*"

Why this matters:

  • file: bypasses npm's workspace resolution and creates a hard relative-path coupling. If calm-suite/ is ever restructured (or if calmguard is consumed outside the monorepo), this breaks in non-obvious ways.
  • Renovate / dependabot treat file: deps differently from workspace deps.
  • The CI workflow's explicit Build @finos/calm-models step (workflow lines added in this PR) is correct either way — the change is purely about how the dep is declared.

Non-blocking observations (no action needed, just calling out)

  1. node-type widening is silent at the UI layer. AGENTS.md is honest about "9 well-known values get first-class UI, any string accepted", but the React Flow custom-node mapping and the compliance heat-map are still keyed off the original 9-value enum, so an aws:lambda node will parse but fall through to a default renderer / bucket as "unknown" in the heat-map. The user story (parse, don't reject) is fixed here — render-side work is a reasonable follow-up issue.

  2. CI test job does not build @finos/calm-models. Workflow adds the build step before lint and before build, but not before test. Currently safe because tests don't import calm-models at runtime (only conformance.ts does, type-only, esbuild strips). Worth a comment in conformance.ts so a future author who adds a runtime import knows to update the test job.

  3. detectCalmVersion returns "1.1" for canonical nested docs missing adrs/decorators/timelines. Parses correctly, but the version label on ParseSuccess.version is technically wrong. No functional impact.

  4. Missing test scenario: a relationship with TWO variant keys (e.g. both connects and interacts). The superRefine rejects this, which is the strictest-vs-canonical-oneOf behavior and worth pinning explicitly.

  5. Makefile is unrelated scope creep (PR body acknowledges, offers to split). The Makefile itself is fine. No strong opinion either way.

Will re-approve once the file:"*" flip lands. Everything else can be a follow-up.

Comment thread package-lock.json
"@ai-sdk/xai": "^3.0.57",
"@dagrejs/dagre": "^2.0.4",
"@finos/calm-cli": "^1.33.0",
"@finos/calm-models": "file:../../calm-models",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Suggested change
"@finos/calm-models": "file:../../calm-models",
"@finos/calm-models": "*",

@gjs-opsflo

"@ai-sdk/xai": "^3.0.57",
"@dagrejs/dagre": "^2.0.4",
"@finos/calm-cli": "^1.33.0",
"@finos/calm-models": "file:../../calm-models",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Suggested change
"@finos/calm-models": "file:../../calm-models",
"@finos/calm-models": "*",

@gjs-opsflo

@gjs-opsflo

Copy link
Copy Markdown
Contributor

👍 Both suggestions are exactly the change requested — please apply.

One heads-up on the package-lock.json:298 edit: rather than hand-applying the suggestion, safer to let npm rewrite the resolution entry. After bumping package.json to "*":

rm -rf node_modules package-lock.json && npm install

(or npm install --package-lock-only if you want to avoid the full reinstall). The validate-lockfile CI workflow checks all platform-specific optional deps are present in the lockfile, and a hand-edit risks drift if npm's resolution metadata for the workspace symlink differs from the literal "*" string. We've been bitten by lockfile regen edge cases before (PR #2661 broke vsce with --force regen), so the full delete-then-install path is the safest.

Once that lands I'll re-review and flip to APPROVE. Thanks for the quick turnaround!

@eddie-knight

Copy link
Copy Markdown
Contributor Author

Looks like #2730 got merged ahead of this one, so I'll need to revisit the entire PR stack.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants