Skip to content

Commit 57b8bda

Browse files
authored
feat(core): orphaned-flow diagnostic for flows with no surfaced steps (#106)
1 parent b76a1e0 commit 57b8bda

3 files changed

Lines changed: 177 additions & 0 deletions

File tree

.changeset/shy-kids-grow.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@intent-framework/core": minor
3+
---
4+
5+
Add `orphaned-flow` diagnostic to `inspectScreen` for flows with no surfaced steps.
6+
7+
`inspectScreen` now reports `severity: "warning"` diagnostics with code `orphaned-flow`
8+
when a flow has one or more steps but none of its steps appear in any surface.

packages/core/src/core.test.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,158 @@ describe("graph diagnostics", () => {
980980
expect(reachDiags[0]?.nodeId).toBe("act_sign_up")
981981
})
982982
})
983+
984+
describe("orphaned-flow diagnostic", () => {
985+
it("does not emit orphaned-flow when there are no flows", () => {
986+
const screenDef = screen("NoFlowsForOrphan", $ => {
987+
const email = $.state.text("email")
988+
const emailAsk = $.ask("Email", email).private()
989+
const login = $.act("Log in")
990+
$.surface("main").contains(emailAsk, login)
991+
})
992+
const inspected = inspectScreen(screenDef)
993+
const diags = inspected.diagnostics.filter(d => d.code === "orphaned-flow")
994+
expect(diags).toHaveLength(0)
995+
})
996+
997+
it("does not emit orphaned-flow for empty flow", () => {
998+
const screenDef = screen("EmptyFlow", $ => {
999+
const email = $.state.text("email")
1000+
const emailAsk = $.ask("Email", email).private()
1001+
const login = $.act("Log in")
1002+
$.surface("main").contains(emailAsk, login)
1003+
$.flow("empty")
1004+
})
1005+
const inspected = inspectScreen(screenDef)
1006+
const diags = inspected.diagnostics.filter(d => d.code === "orphaned-flow")
1007+
expect(diags).toHaveLength(0)
1008+
})
1009+
1010+
it("emits orphaned-flow when all flow steps are unsurfaced", () => {
1011+
const screenDef = screen("AllStepsUnsurfaced", $ => {
1012+
const email = $.state.text("email")
1013+
const pwd = $.state.text("password")
1014+
const emailAsk = $.ask("Email", email).private()
1015+
const pwdAsk = $.ask("Password", pwd).private()
1016+
const login = $.act("Log in")
1017+
$.surface("main").contains(login)
1018+
$.flow("login").startsWith(emailAsk).then(pwdAsk)
1019+
})
1020+
const inspected = inspectScreen(screenDef)
1021+
const diags = inspected.diagnostics.filter(d => d.code === "orphaned-flow")
1022+
expect(diags).toHaveLength(1)
1023+
expect(diags[0]?.severity).toBe("warning")
1024+
expect(diags[0]?.code).toBe("orphaned-flow")
1025+
expect(diags[0]?.message).toBe('"login" has no surfaced steps.')
1026+
})
1027+
1028+
it("does not emit orphaned-flow when at least one flow step is surfaced", () => {
1029+
const screenDef = screen("OneStepSurfaced", $ => {
1030+
const email = $.state.text("email")
1031+
const pwd = $.state.text("password")
1032+
const emailAsk = $.ask("Email", email).private()
1033+
const pwdAsk = $.ask("Password", pwd).private()
1034+
const login = $.act("Log in")
1035+
$.surface("main").contains(emailAsk, login)
1036+
$.flow("login").startsWith(emailAsk).then(pwdAsk).then(login)
1037+
})
1038+
const inspected = inspectScreen(screenDef)
1039+
const diags = inspected.diagnostics.filter(d => d.code === "orphaned-flow")
1040+
expect(diags).toHaveLength(0)
1041+
})
1042+
1043+
it("multiple flows report only orphaned flows", () => {
1044+
const screenDef = screen("MultipleFlowsOrphan", $ => {
1045+
const email = $.state.text("email")
1046+
const pwd = $.state.text("password")
1047+
const emailAsk = $.ask("Email", email).private()
1048+
const pwdAsk = $.ask("Password", pwd).private()
1049+
const login = $.act("Log in")
1050+
const signup = $.act("Sign up")
1051+
$.surface("main").contains(emailAsk, login)
1052+
$.flow("a").startsWith(emailAsk).then(login)
1053+
$.flow("b").startsWith(pwdAsk).then(signup)
1054+
})
1055+
const inspected = inspectScreen(screenDef)
1056+
const diags = inspected.diagnostics.filter(d => d.code === "orphaned-flow")
1057+
expect(diags).toHaveLength(1)
1058+
expect(diags[0]?.message).toBe('"b" has no surfaced steps.')
1059+
})
1060+
1061+
it("diagnostic includes nested flow.flowNodeId and flow.flowSemanticNodeId", () => {
1062+
const screenDef = screen("OrphanedFlowMeta", $ => {
1063+
const email = $.state.text("email")
1064+
const pwd = $.state.text("password")
1065+
const emailAsk = $.ask("Email", email).private()
1066+
const pwdAsk = $.ask("Password", pwd).private()
1067+
const login = $.act("Log in")
1068+
$.surface("main").contains(login)
1069+
$.flow("login").startsWith(emailAsk).then(pwdAsk)
1070+
})
1071+
const inspected = inspectScreen(screenDef)
1072+
const diags = inspected.diagnostics.filter(d => d.code === "orphaned-flow")
1073+
expect(diags).toHaveLength(1)
1074+
expect(diags[0]?.flow?.flowNodeId).toBe("flow_login")
1075+
expect(diags[0]?.flow?.flowSemanticNodeId).toBe("flow:login")
1076+
})
1077+
1078+
it("diagnostic leaves nodeId and semanticNodeId undefined", () => {
1079+
const screenDef = screen("OrphanedNoNodeId", $ => {
1080+
const email = $.state.text("email")
1081+
const pwd = $.state.text("password")
1082+
const emailAsk = $.ask("Email", email).private()
1083+
const pwdAsk = $.ask("Password", pwd).private()
1084+
const login = $.act("Log in")
1085+
$.surface("main").contains(login)
1086+
$.flow("login").startsWith(emailAsk).then(pwdAsk)
1087+
})
1088+
const inspected = inspectScreen(screenDef)
1089+
const diags = inspected.diagnostics.filter(d => d.code === "orphaned-flow")
1090+
expect(diags).toHaveLength(1)
1091+
expect(diags[0]?.nodeId).toBeUndefined()
1092+
expect(diags[0]?.semanticNodeId).toBeUndefined()
1093+
})
1094+
1095+
it("deterministic ordering across repeated inspectScreen calls", () => {
1096+
const screenDef = screen("OrphanedDeterministic", $ => {
1097+
const email = $.state.text("email")
1098+
const pwd = $.state.text("password")
1099+
const emailAsk = $.ask("Email", email).private()
1100+
const pwdAsk = $.ask("Password", pwd).private()
1101+
const login = $.act("Log in")
1102+
const signup = $.act("Sign up")
1103+
$.surface("main").contains(login)
1104+
$.flow("a").startsWith(emailAsk).then(login)
1105+
$.flow("b").startsWith(pwdAsk).then(signup)
1106+
})
1107+
const first = inspectScreen(screenDef)
1108+
const second = inspectScreen(screenDef)
1109+
expect(first.diagnostics.map(d => d.code)).toEqual(second.diagnostics.map(d => d.code))
1110+
})
1111+
1112+
it("coexists with flow-step-not-surfaced without changing its behavior", () => {
1113+
const screenDef = screen("CoexistOrphan", $ => {
1114+
const email = $.state.text("email")
1115+
const pwd = $.state.text("password")
1116+
const emailAsk = $.ask("Email", email).private()
1117+
const pwdAsk = $.ask("Password", pwd).private()
1118+
const login = $.act("Log in")
1119+
const signup = $.act("Sign up")
1120+
$.surface("main").contains(login)
1121+
$.flow("a").startsWith(emailAsk).then(login)
1122+
$.flow("b").startsWith(pwdAsk).then(signup)
1123+
})
1124+
const inspected = inspectScreen(screenDef)
1125+
const orphaned = inspected.diagnostics.filter(d => d.code === "orphaned-flow")
1126+
expect(orphaned).toHaveLength(1)
1127+
expect(orphaned[0]?.message).toBe('"b" has no surfaced steps.')
1128+
const flowStepNotSurfaced = inspected.diagnostics.filter(d => d.code === "flow-step-not-surfaced")
1129+
expect(flowStepNotSurfaced).toHaveLength(3)
1130+
expect(flowStepNotSurfaced[0]?.nodeId).toBe("ask_email")
1131+
expect(flowStepNotSurfaced[1]?.nodeId).toBe("ask_password")
1132+
expect(flowStepNotSurfaced[2]?.nodeId).toBe("act_sign_up")
1133+
})
1134+
})
9831135
})
9841136

9851137
describe("resource", () => {

packages/core/src/graph.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,23 @@ function computeDiagnostics<TServices extends object = DefaultScreenServices>(
189189
}
190190
}
191191

192+
// orphaned-flow: flow has steps but none are surfaced
193+
for (const flow of screenDef.flows) {
194+
if (flow.steps.length > 0) {
195+
const hasSurfacedStep = flow.steps.some(step => surfacedNodeIds.has(step.node.id))
196+
if (!hasSurfacedStep) {
197+
diagnostics.push({
198+
severity: "warning",
199+
code: "orphaned-flow",
200+
message: `"${flow.name}" has no surfaced steps.`,
201+
flow: {
202+
flowNodeId: flow.id,
203+
},
204+
})
205+
}
206+
}
207+
}
208+
192209
for (const ask of screenDef.asks) {
193210
if (surfacedNodeIds.has(ask.id) && !flowNodeIds.has(ask.id)) {
194211
diagnostics.push({

0 commit comments

Comments
 (0)