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()) {
|
if (chain.getMatchedTransitions() != null && !chain.getMatchedTransitions().isEmpty()) {
|
||||||
LinkResolution resolution = chain.getLinkResolution();
|
LinkResolution resolution = chain.getLinkResolution();
|
||||||
return resolution == null || resolution == LinkResolution.RESOLVED;
|
return resolution == null
|
||||||
|
|| resolution == LinkResolution.RESOLVED
|
||||||
|
|| resolution == LinkResolution.AMBIGUOUS_WIDEN;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,12 +377,21 @@
|
|||||||
return c.linkResolution === 'RESOLVED' || c.linkResolution == null;
|
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) {
|
function isUnresolvedExternalChain(c) {
|
||||||
return c.linkResolution === 'UNRESOLVED_EXTERNAL';
|
return c.linkResolution === 'UNRESOLVED_EXTERNAL';
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSidebarChain(c) {
|
function isSidebarChain(c) {
|
||||||
return isLifecycleChain(c) || isResolvedChain(c) || isUnresolvedExternalChain(c);
|
return isLifecycleChain(c) || isHighlightableChain(c) || isUnresolvedExternalChain(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
function chainsForEntryPoint(ep) {
|
function chainsForEntryPoint(ep) {
|
||||||
@@ -397,6 +406,10 @@
|
|||||||
return chainsForEntryPoint(ep).filter(isResolvedChain);
|
return chainsForEntryPoint(ep).filter(isResolvedChain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function highlightableChainsForEntryPoint(ep) {
|
||||||
|
return chainsForEntryPoint(ep).filter(isHighlightableChain);
|
||||||
|
}
|
||||||
|
|
||||||
function populateSidebar() {
|
function populateSidebar() {
|
||||||
const list = document.getElementById('ep-list');
|
const list = document.getElementById('ep-list');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
@@ -421,12 +434,15 @@
|
|||||||
if (lifecycleChain) {
|
if (lifecycleChain) {
|
||||||
const label = formatLifecycleLabel(lifecycleChain.triggerPoint.event);
|
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>`;
|
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>`;
|
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 {
|
} 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: ';
|
triggersHtml += 'Triggers: ';
|
||||||
const addedEvents = new Set();
|
const addedEvents = new Set();
|
||||||
chains.filter(isResolvedChain).forEach(c => {
|
chains.filter(isHighlightableChain).forEach(c => {
|
||||||
c.matchedTransitions.forEach(t => {
|
c.matchedTransitions.forEach(t => {
|
||||||
const evName = t.event || "";
|
const evName = t.event || "";
|
||||||
if (!addedEvents.has(evName)) {
|
if (!addedEvents.has(evName)) {
|
||||||
@@ -470,7 +486,7 @@
|
|||||||
|
|
||||||
function highlightEntryPoint(ep) {
|
function highlightEntryPoint(ep) {
|
||||||
clearHighlights();
|
clearHighlights();
|
||||||
const chains = resolvedChainsForEntryPoint(ep);
|
const chains = highlightableChainsForEntryPoint(ep);
|
||||||
|
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
let steps = [];
|
let steps = [];
|
||||||
@@ -479,7 +495,12 @@
|
|||||||
const key = buildLinkKey(t.sourceState, t.event);
|
const key = buildLinkKey(t.sourceState, t.event);
|
||||||
if (!key || seen.has(key)) return;
|
if (!key || seen.has(key)) return;
|
||||||
seen.add(key);
|
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")
|
.className("com.example.web.OrderController")
|
||||||
.methodName("suspend")
|
.methodName("suspend")
|
||||||
.build();
|
.build();
|
||||||
|
EntryPoint ambiguousEp = EntryPoint.builder()
|
||||||
|
.type(EntryPoint.Type.CUSTOM)
|
||||||
|
.name("ambiguous")
|
||||||
|
.className("com.example.web.AmbiguousController")
|
||||||
|
.methodName("trigger")
|
||||||
|
.build();
|
||||||
|
|
||||||
CallChain resolvedChain = CallChain.builder()
|
CallChain resolvedChain = CallChain.builder()
|
||||||
.entryPoint(resolvedEp)
|
.entryPoint(resolvedEp)
|
||||||
@@ -318,12 +324,7 @@ class HtmlTransitionLinkConsistencyTest {
|
|||||||
.build())
|
.build())
|
||||||
.build();
|
.build();
|
||||||
CallChain ambiguousChain = CallChain.builder()
|
CallChain ambiguousChain = CallChain.builder()
|
||||||
.entryPoint(EntryPoint.builder()
|
.entryPoint(ambiguousEp)
|
||||||
.type(EntryPoint.Type.CUSTOM)
|
|
||||||
.name("ambiguous")
|
|
||||||
.className("com.example.web.AmbiguousController")
|
|
||||||
.methodName("trigger")
|
|
||||||
.build())
|
|
||||||
.linkResolution(LinkResolution.AMBIGUOUS_WIDEN)
|
.linkResolution(LinkResolution.AMBIGUOUS_WIDEN)
|
||||||
.triggerPoint(TriggerPoint.builder()
|
.triggerPoint(TriggerPoint.builder()
|
||||||
.event("com.example.order.OrderEvent.PAY")
|
.event("com.example.order.OrderEvent.PAY")
|
||||||
@@ -347,7 +348,7 @@ class HtmlTransitionLinkConsistencyTest {
|
|||||||
.startStates(Set.of("com.example.order.OrderState.NEW"))
|
.startStates(Set.of("com.example.order.OrderState.NEW"))
|
||||||
.endStates(Set.of("com.example.order.OrderState.PAID"))
|
.endStates(Set.of("com.example.order.OrderState.PAID"))
|
||||||
.metadata(CodebaseMetadata.builder()
|
.metadata(CodebaseMetadata.builder()
|
||||||
.entryPoints(List.of(resolvedEp, unresolvedEp, noMatchEp))
|
.entryPoints(List.of(resolvedEp, unresolvedEp, noMatchEp, ambiguousEp))
|
||||||
.callChains(List.of(resolvedChain, unresolvedChain, noMatchChain, ambiguousChain))
|
.callChains(List.of(resolvedChain, unresolvedChain, noMatchChain, ambiguousChain))
|
||||||
.build())
|
.build())
|
||||||
.build();
|
.build();
|
||||||
@@ -355,13 +356,63 @@ class HtmlTransitionLinkConsistencyTest {
|
|||||||
String html = new HtmlExporter().export(result, ExportOptions.builder().includeMetadataPane(true).build());
|
String html = new HtmlExporter().export(result, ExportOptions.builder().includeMetadataPane(true).build());
|
||||||
|
|
||||||
assertThat(html).contains("function isResolvedChain(c)");
|
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 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("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 === 'UNRESOLVED_EXTERNAL'");
|
||||||
assertThat(html).contains("linkResolution === 'RESOLVED'");
|
assertThat(html).contains("linkResolution === 'RESOLVED'");
|
||||||
|
assertThat(html).contains("linkResolution === 'AMBIGUOUS_WIDEN'");
|
||||||
assertThat(html).contains("id=\"sidebar\"");
|
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. */
|
/** Mirrors {@code template.html} link-key derivation for regression checks. */
|
||||||
|
|||||||
Reference in New Issue
Block a user