Skip to content

fix(calm-suite): read flow transition labels from the schema description field - #2939

Open
gjs-opsflo wants to merge 4 commits into
finos:mainfrom
gjs-opsflo:fix/flow-transition-description
Open

fix(calm-suite): read flow transition labels from the schema description field#2939
gjs-opsflo wants to merge 4 commits into
finos:mainfrom
gjs-opsflo:fix/flow-transition-description

Conversation

@gjs-opsflo

Copy link
Copy Markdown
Contributor

Description

The flow overlay in @calmstudio/diagram read transition labels from a legacy summary field, but the flow meta-schema (calm/release/1.2/meta/flow.json) defines description on transitions. A schema-correct flow document with no summary rendered its sequence-badge tooltips as undefined.

Fix: prefer the schema's description, keep summary as a fallback for older files. Found while building the learn-tutorials PoC (#2928) — its demo files carried both fields as a workaround, which this makes unnecessary.

Side effect: removes two pre-existing typecheck errors in flowOverlay.ts (the typed access to the non-schema summary field).

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)

Affected Components

  • CALM Studio (calm-studio/) — packages/web-component

Testing

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

Evidence: new test renders a schema-only flow (no summary) and asserts the badge carries the description text with no undefined in the output; suite 54/54; typecheck error count drops by two.

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

…ion field

The flow meta-schema defines description on transitions; summary was a
legacy CalmStudio field. Schema-only flow documents previously rendered
badge tooltips as undefined. Prefer description, keep summary as a
fallback for older files.

Signed-off-by: Test User <gjs@opsflow.sh>
Transitions sharing a relationship now spread along the edge path
((k+1)/(n+1) of path length) instead of stacking on the midpoint, and
destination-to-source transitions render hollow badges (white fill, blue
ring) so request/response direction is readable without the animation.

Signed-off-by: Test User <gjs@opsflow.sh>
Reverse (destination-to-source) badges shift perpendicular to the edge
so request/response pairs stay separated even on edges shorter than a
badge diameter. Verified visually on the tutorial inventory flow: all
five sequence numbers legible, hollow badges beside their solid pair.

Signed-off-by: Test User <gjs@opsflow.sh>
@gjs-opsflo
gjs-opsflo force-pushed the fix/flow-transition-description branch from 0f6f8f1 to f48eaac Compare August 6, 2026 06:49

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

Overview

flowOverlay.ts read transition.summary — a legacy CalmStudio field absent from CalmFlowTransitionSchema — so schema-valid flows rendered data-summary="undefined" and <title>undefined</title>. Preferring transition.description with a cast-based summary fallback is correct: calm-models/src/types/flow-types.ts:6 confirms description: string is the schema field and summary doesn't exist, which is also the source of the two typecheck errors this removes.

The diff also carries badge-placement and styling work: transitions sharing a relationship now spread along the path at (k+1)/(n+1) rather than stacking at the midpoint, reverse transitions shift into a perpendicular "return lane" and render hollow via a new flow-badge-reverse class, and two geometry helpers (pointAtFraction, pathNormalAt) support that. It's coherent with the bug being fixed — the same request/response demo files that exposed the undefined labels are exactly where stacked badges bite. Worth a line in the PR body so the visual change is discoverable from the changelog, but no objection to it riding along.

Correctness

Inline comments cover the specifics. In summary:

  • The lane offset applies to every reverse badge, not just crowded ones — a lone destination-to-source transition's badge now floats 22px off its edge.
  • The perpendicular direction is arbitrary (always (-ty, tx)), so which side a badge lands on depends on layout point ordering; nothing prevents it landing over a node.
  • Badge spread follows document order rather than sequence-number.
  • pointAtFraction emits raw floats, so output now contains cx="140.86000000000001"-style values where layout integers used to be.
  • Pre-existing but adjacent: ${transition['sequence-number']} is interpolated into text content unescaped. It's typed number, but these are parsed JSON documents, so a string value would inject markup — cheap to route through escapeAttr while you're in this code.

Code quality

  • pointAtFraction and pathNormalAt duplicate the segment-length walk verbatim, and the copies diverge on degenerate input (pointAtFraction returns first when total === 0; pathNormalAt returns undefined). One function returning { point, normal } removes both the duplication and the divergence.
  • The attribute is still data-summary while the value now comes from description. I found no reader in calm-studio/ — only the :global(.flow-badge) cursor rule at CalmDiagram.svelte:234 — so either rename to data-description or drop it, since <title> already supplies the tooltip.
  • Hard-coded #ffffff for the hollow badge: the component supports :host([theme="dark"]) with --calm-bg: #1e1e1e (CalmDiagram.svelte:228), so on dark the "hollow" reading inverts to a white disc. The file hard-codes #3b82f6 throughout already, so this is existing debt rather than new — follow-up material.
  • A short note on the (transition as { summary?: string }) cast saying when the fallback can be retired would help future cleanup.

Test coverage

  • The core regression test is well-targeted: schema-only flow, asserts the description text appears and undefined doesn't.
  • The summary fallback is untested. Nothing asserts a legacy summary-only transition still produces a label — existing fixtures use summary but only assert on animateMotion, keyPoints, and badge counts, never label text. Backward compatibility is the stated reason for keeping the cast, so it should have a test, plus one asserting description wins when both are present.
  • The geometry tests assert only inequality (centres differ, distance >= 18) and would pass under badly wrong placement. Pinning an expected fraction point on a known polyline would lock the behaviour down.
  • Minor: the new test counts /class="flow-badge/g (unterminated) while the existing test near line 226 counts class="flow-badge" (terminated), which now silently excludes reverse badges — it passes only because that fixture is forward-only.
  • expect(svg).toContain('fill="#ffffff"') couples to a literal colour; asserting flow-badge-reverse is enough.
  • Fixtures still put summary on CalmFlow-typed literals, which remain excess-property type errors. The claimed two-error drop is real for the production file; the test file's errors could be cleared in the same pass.

Verdict

Approve-with-comments. The label fix is correct and the badge work is a reasonable companion. Worth addressing before merge: gate the lane offset to the crowded case, and add a test for the summary fallback. Float precision, helper duplication, and the data-summary naming are fine as follow-ups.

Comment thread calm-studio/packages/web-component/src/render/flowOverlay.ts Outdated
Comment thread calm-studio/packages/web-component/src/render/flowOverlay.ts
Comment thread calm-studio/packages/web-component/src/render/flowOverlay.ts Outdated
Comment thread calm-studio/packages/web-component/src/render/flowOverlay.ts Outdated
if (a === undefined || b === undefined || len === 0) return undefined;
const tx = (b.x - a.x) / len;
const ty = (b.y - a.y) / len;
return { x: -ty, y: tx };

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.

The normal is always (-ty, tx), so which side of the edge a reverse badge lands on depends purely on the layout engine's point ordering — nothing stops it landing on top of a node, another edge, or off-canvas. A comment acknowledging that is probably enough for now; biasing away from the diagram centroid would be the fuller fix.

Separately, this function re-walks the segment lengths exactly as pointAtFraction does, and the two copies handle degenerate input differently (pointAtFraction returns first when total === 0, this returns undefined). One function returning { point, normal } would remove the duplication and the divergence.

Comment thread calm-studio/packages/web-component/src/render/flowOverlay.test.ts
Badge spread ranks by sequence-number rather than document order; the
return-lane offset applies only when a shared edge is actually crowded
(along-path spacing under a badge diameter), so a lone reverse badge
stays on its line; coordinates round to 2dp; the badge data attribute is
renamed data-description to match its content; the two path helpers
share one polyline walk with consistent degenerate handling; and the
label fallback chain gains legacy-summary and description-wins tests.

Signed-off-by: Test User <gjs@opsflow.sh>
@gjs-opsflo

Copy link
Copy Markdown
Contributor Author

Thanks @markscott-ms — all six addressed in 65c211d6:

  • Sequence ordering: spread rank now sorts by sequence-number within each relationship (test: out-of-order transition arrays place badge 1 before badge 2 along the path).
  • Over-applied lane offset: gated on the crowded case — count > 1 AND along-path spacing under a badge diameter. A lone reverse badge stays on its line (test added).
  • Float coordinates: rounded to 2dp at all emission points (test asserts no 3+-decimal runs).
  • data-summary: renamed data-description to match its content; confirmed no readers beyond the class-based cursor rule.
  • Normal side / duplicated walk: both helpers now share one walkPolyline with consistent degenerate handling, and the unbiased-side limitation is acknowledged in a comment, with centroid-bias noted as the fuller fix if it shows up in practice.
  • Label test gaps: added legacy summary-only coverage and a description-wins-when-both-present test.

Suite 64/64, typecheck unchanged.

@gjs-opsflo

Copy link
Copy Markdown
Contributor Author

@markscott-ms gentle nudge — all six review points were addressed in 65c211d (sequence ordering, crowding gate, rounding, data-description, unified path walk, label tests). Ready for another look whenever you have time. No rush, and thanks again for the detailed first pass.

@rocketstack-matt rocketstack-matt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The six points from the earlier review are addressed in 65c211d (verified against the diff + tests: sequence-number ordering, crowding-gated lane offset, 2dp rounding, data-description rename, shared walkPolyline, and both label-fallback tests). Three smaller items below.

` <title>${escapeAttr(transition.summary)}</title>`,
`<g class="${badgeClass}" data-description="${escapeAttr(transitionLabel)}">`,
` <circle cx="${midX}" cy="${midY}" r="10" fill="${circleFill}"${circleExtra}/>`,
` <text x="${midX}" y="${midY}" fill="${numberFill}" font-size="9" font-weight="bold" text-anchor="middle" dominant-baseline="central">${transition['sequence-number']}</text>`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

transition['sequence-number'] goes into the badge text unescaped, while every other dynamic value here (and in nodeRenderer.ts) is routed through an escape helper. The type says number, but this is parsed JSON reaching {@html svgContent} in CalmDiagram.svelte:166 — a document with a string in that field injects markup. Raised in the original review body as a while-you're-here item; still open.

Suggested change
` <text x="${midX}" y="${midY}" fill="${numberFill}" font-size="9" font-weight="bold" text-anchor="middle" dominant-baseline="central">${transition['sequence-number']}</text>`,
` <text x="${midX}" y="${midY}" fill="${numberFill}" font-size="9" font-weight="bold" text-anchor="middle" dominant-baseline="central">${escapeAttr(String(transition['sequence-number']))}</text>`,

return Math.round(n * 100) / 100;
}

function escapeAttr(str: string): string {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Pre-existing, not introduced here: this duplicates nodeRenderer.ts's escapeXml but doesn't escape '. Since this file is already being touched, worth exporting escapeXml from nodeRenderer.ts and importing it here instead of maintaining two slightly different escaping helpers in the same package. Non-blocking.

if (seg.len === 0) return undefined;
const tx = (seg.b.x - seg.a.x) / seg.len;
const ty = (seg.b.y - seg.a.y) / seg.len;
return { x: -ty, y: tx };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Following up on the duplication point from the earlier review: walkPolyline now shares the segment/length precompute, but pointOnWalk and normalOnWalk still each re-walk the segment list to find the target fraction. A single function returning { point, normal } (as originally suggested) would still collapse that. Non-blocking.

@rocketstack-matt

Copy link
Copy Markdown
Member

@gjs-opsflo nudge

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.

3 participants