Surface ambiguous widen chains in HTML explorer
AMBIGUOUS_WIDEN call chains with matched transitions now appear in the sidebar with an Ambiguous link badge, use dashed highlight styling, and participate in endpoint hover when link keys are available. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -140,7 +140,9 @@ public class HtmlExporter implements StateMachineExporter {
|
||||
}
|
||||
if (chain.getMatchedTransitions() != null && !chain.getMatchedTransitions().isEmpty()) {
|
||||
LinkResolution resolution = chain.getLinkResolution();
|
||||
return resolution == null || resolution == LinkResolution.RESOLVED;
|
||||
return resolution == null
|
||||
|| resolution == LinkResolution.RESOLVED
|
||||
|| resolution == LinkResolution.AMBIGUOUS_WIDEN;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -377,12 +377,21 @@
|
||||
return c.linkResolution === 'RESOLVED' || c.linkResolution == null;
|
||||
}
|
||||
|
||||
function isAmbiguousWidenChain(c) {
|
||||
return c.linkResolution === 'AMBIGUOUS_WIDEN'
|
||||
&& c.matchedTransitions && c.matchedTransitions.length > 0;
|
||||
}
|
||||
|
||||
function isHighlightableChain(c) {
|
||||
return isResolvedChain(c) || isAmbiguousWidenChain(c);
|
||||
}
|
||||
|
||||
function isUnresolvedExternalChain(c) {
|
||||
return c.linkResolution === 'UNRESOLVED_EXTERNAL';
|
||||
}
|
||||
|
||||
function isSidebarChain(c) {
|
||||
return isLifecycleChain(c) || isResolvedChain(c) || isUnresolvedExternalChain(c);
|
||||
return isLifecycleChain(c) || isHighlightableChain(c) || isUnresolvedExternalChain(c);
|
||||
}
|
||||
|
||||
function chainsForEntryPoint(ep) {
|
||||
@@ -397,6 +406,10 @@
|
||||
return chainsForEntryPoint(ep).filter(isResolvedChain);
|
||||
}
|
||||
|
||||
function highlightableChainsForEntryPoint(ep) {
|
||||
return chainsForEntryPoint(ep).filter(isHighlightableChain);
|
||||
}
|
||||
|
||||
function populateSidebar() {
|
||||
const list = document.getElementById('ep-list');
|
||||
if (!list) return;
|
||||
@@ -421,12 +434,15 @@
|
||||
if (lifecycleChain) {
|
||||
const label = formatLifecycleLabel(lifecycleChain.triggerPoint.event);
|
||||
triggersHtml += `<span class="badge lifecycle" style="font-size: 0.65rem; padding: 2px 6px; background:#ecfdf5; color:#047857; border:1px solid #a7f3d0;">${label}</span>`;
|
||||
} else if (chains.some(isUnresolvedExternalChain) && !chains.some(isResolvedChain)) {
|
||||
} else if (chains.some(isUnresolvedExternalChain) && !chains.some(isHighlightableChain)) {
|
||||
triggersHtml += `<span class="badge unresolved-external" style="font-size: 0.65rem; padding: 2px 6px; background:#fff7ed; color:#c2410c; border:1px solid #fed7aa;">Unresolved external</span>`;
|
||||
} else {
|
||||
if (chains.some(isAmbiguousWidenChain)) {
|
||||
triggersHtml += `<span class="badge ambiguous-widen" style="font-size: 0.65rem; padding: 2px 6px; background:#fefce8; color:#a16207; border:1px solid #fde68a; margin-right:4px;">Ambiguous link</span>`;
|
||||
}
|
||||
triggersHtml += 'Triggers: ';
|
||||
const addedEvents = new Set();
|
||||
chains.filter(isResolvedChain).forEach(c => {
|
||||
chains.filter(isHighlightableChain).forEach(c => {
|
||||
c.matchedTransitions.forEach(t => {
|
||||
const evName = t.event || "";
|
||||
if (!addedEvents.has(evName)) {
|
||||
@@ -470,7 +486,7 @@
|
||||
|
||||
function highlightEntryPoint(ep) {
|
||||
clearHighlights();
|
||||
const chains = resolvedChainsForEntryPoint(ep);
|
||||
const chains = highlightableChainsForEntryPoint(ep);
|
||||
|
||||
const seen = new Set();
|
||||
let steps = [];
|
||||
@@ -479,7 +495,12 @@
|
||||
const key = buildLinkKey(t.sourceState, t.event);
|
||||
if (!key || seen.has(key)) return;
|
||||
seen.add(key);
|
||||
steps.push({ event: t.event, source: t.sourceState, ambiguous: c.triggerPoint && c.triggerPoint.ambiguous });
|
||||
steps.push({
|
||||
event: t.event,
|
||||
source: t.sourceState,
|
||||
ambiguous: c.linkResolution === 'AMBIGUOUS_WIDEN'
|
||||
|| (c.triggerPoint && c.triggerPoint.ambiguous)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -283,6 +283,12 @@ class HtmlTransitionLinkConsistencyTest {
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("suspend")
|
||||
.build();
|
||||
EntryPoint ambiguousEp = EntryPoint.builder()
|
||||
.type(EntryPoint.Type.CUSTOM)
|
||||
.name("ambiguous")
|
||||
.className("com.example.web.AmbiguousController")
|
||||
.methodName("trigger")
|
||||
.build();
|
||||
|
||||
CallChain resolvedChain = CallChain.builder()
|
||||
.entryPoint(resolvedEp)
|
||||
@@ -318,12 +324,7 @@ class HtmlTransitionLinkConsistencyTest {
|
||||
.build())
|
||||
.build();
|
||||
CallChain ambiguousChain = CallChain.builder()
|
||||
.entryPoint(EntryPoint.builder()
|
||||
.type(EntryPoint.Type.CUSTOM)
|
||||
.name("ambiguous")
|
||||
.className("com.example.web.AmbiguousController")
|
||||
.methodName("trigger")
|
||||
.build())
|
||||
.entryPoint(ambiguousEp)
|
||||
.linkResolution(LinkResolution.AMBIGUOUS_WIDEN)
|
||||
.triggerPoint(TriggerPoint.builder()
|
||||
.event("com.example.order.OrderEvent.PAY")
|
||||
@@ -347,7 +348,7 @@ class HtmlTransitionLinkConsistencyTest {
|
||||
.startStates(Set.of("com.example.order.OrderState.NEW"))
|
||||
.endStates(Set.of("com.example.order.OrderState.PAID"))
|
||||
.metadata(CodebaseMetadata.builder()
|
||||
.entryPoints(List.of(resolvedEp, unresolvedEp, noMatchEp))
|
||||
.entryPoints(List.of(resolvedEp, unresolvedEp, noMatchEp, ambiguousEp))
|
||||
.callChains(List.of(resolvedChain, unresolvedChain, noMatchChain, ambiguousChain))
|
||||
.build())
|
||||
.build();
|
||||
@@ -355,13 +356,63 @@ class HtmlTransitionLinkConsistencyTest {
|
||||
String html = new HtmlExporter().export(result, ExportOptions.builder().includeMetadataPane(true).build());
|
||||
|
||||
assertThat(html).contains("function isResolvedChain(c)");
|
||||
assertThat(html).contains("function isAmbiguousWidenChain(c)");
|
||||
assertThat(html).contains("function isHighlightableChain(c)");
|
||||
assertThat(html).contains("function isSidebarChain(c)");
|
||||
assertThat(html).contains("function resolvedChainsForEntryPoint(ep)");
|
||||
assertThat(html).contains("function highlightableChainsForEntryPoint(ep)");
|
||||
assertThat(html).contains("Ambiguous link");
|
||||
assertThat(html).contains("active-path-ambiguous");
|
||||
assertThat(html).contains("Unresolved external");
|
||||
assertThat(html).contains("const chains = resolvedChainsForEntryPoint(ep)");
|
||||
assertThat(html).contains("const chains = highlightableChainsForEntryPoint(ep)");
|
||||
assertThat(html).contains("linkResolution === 'UNRESOLVED_EXTERNAL'");
|
||||
assertThat(html).contains("linkResolution === 'RESOLVED'");
|
||||
assertThat(html).contains("linkResolution === 'AMBIGUOUS_WIDEN'");
|
||||
assertThat(html).contains("id=\"sidebar\"");
|
||||
assertThat(html).contains("#link_OrderState_NEW__OrderEvent_PAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ambiguousWidenHtmlShouldExposeMatchedTransitionLinkKeys() {
|
||||
EntryPoint ambiguousEp = EntryPoint.builder()
|
||||
.type(EntryPoint.Type.CUSTOM)
|
||||
.name("ambiguous")
|
||||
.className("com.example.web.AmbiguousController")
|
||||
.methodName("trigger")
|
||||
.build();
|
||||
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("com.example.config.OrderStateMachineConfiguration")
|
||||
.stateTypeFqn("com.example.order.OrderState")
|
||||
.eventTypeFqn("com.example.order.OrderEvent")
|
||||
.transitions(List.of(shortTransition(
|
||||
"com.example.order.OrderEvent.PAY",
|
||||
"com.example.order.OrderState.NEW",
|
||||
"com.example.order.OrderState.PAID")))
|
||||
.startStates(Set.of("com.example.order.OrderState.NEW"))
|
||||
.endStates(Set.of("com.example.order.OrderState.PAID"))
|
||||
.metadata(CodebaseMetadata.builder()
|
||||
.entryPoints(List.of(ambiguousEp))
|
||||
.callChains(List.of(CallChain.builder()
|
||||
.entryPoint(ambiguousEp)
|
||||
.linkResolution(LinkResolution.AMBIGUOUS_WIDEN)
|
||||
.triggerPoint(TriggerPoint.builder()
|
||||
.event("com.example.order.OrderEvent.PAY")
|
||||
.ambiguous(true)
|
||||
.build())
|
||||
.matchedTransitions(List.of(MatchedTransition.builder()
|
||||
.sourceState("com.example.order.OrderState.NEW")
|
||||
.targetState("com.example.order.OrderState.PAID")
|
||||
.event("com.example.order.OrderEvent.PAY")
|
||||
.build()))
|
||||
.build()))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
String html = new HtmlExporter().export(result, ExportOptions.builder().includeMetadataPane(true).build());
|
||||
|
||||
assertThat(html).contains("Ambiguous link");
|
||||
assertThat(html).contains("#link_OrderState_NEW__OrderEvent_PAY");
|
||||
assertThat(html).contains("com.example.web.AmbiguousController");
|
||||
}
|
||||
|
||||
/** Mirrors {@code template.html} link-key derivation for regression checks. */
|
||||
|
||||
Reference in New Issue
Block a user