Skip to content

Commit b58c9f1

Browse files
DylanMerigaudclaude
andcommitted
#3 Grey the whole workflow on a blocked run (it never routed)
A blocked run (duplicate) fails a control at intake before any gate routes, so the block step emits empty approval `steps`. The graph then drew the full workflow with no statuses, reading as if the bill routed normally through Manager -> Director -> Post, which is misleading. Detect a blocked run (approval outcome `blocked`, or matching verdict `duplicate`) and mark every step `skipped`, so the canvas greys out to show the workflow was NOT taken; the red outcome banner carries the why. Reuses the existing `skipped` node style (already greyed at 45%) — no new concept, no layout change. NOTE: typechecks and reuses a tested style path; not confirmed live (the blocked run's vision extraction kept it mid-run past the screenshot window). To verify: run INV-2041 (the duplicate) and check the graph greys once it resolves. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7a2f641 commit b58c9f1

1 file changed

Lines changed: 24 additions & 0 deletions

File tree

components/dashboard.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,17 @@ const readRunGraph = (
121121
}
122122
}
123123
}
124+
// A BLOCKED run (duplicate) never entered the workflow: a control failed at intake
125+
// before any gate could route, so the block step emits an empty `steps`. Drawing the
126+
// full workflow with no statuses reads as if the bill routed normally, which is
127+
// wrong. Grey EVERY node (status "skipped") so the canvas shows the workflow was not
128+
// taken, and let the red outcome banner carry the why. Detected from the outcome
129+
// (approval `blocked`) or the matching verdict (`duplicate`).
130+
if (isBlockedRun(trace, approval.data)) {
131+
const skipped: StepStatuses = {};
132+
for (const s of parsed.data.steps) skipped[s.id] = "skipped";
133+
return { workflow: parsed.data, statuses: skipped };
134+
}
124135
// Resolve the LINEAR path THIS invoice takes: evaluate each gate's condition
125136
// against the matched invoice and drop the ones that don't apply (rewiring edges),
126137
// so the reviewer sees Manager → Post, not an ambiguous Manager → Director AND
@@ -131,6 +142,19 @@ const readRunGraph = (
131142
return { workflow: resolvePath(parsed.data, ctx), statuses };
132143
};
133144

145+
/** True when the run was blocked at a pre-workflow control (a duplicate), so nothing
146+
routed. Reads the approval event's `outcome` (blocked) or the matching verdict. */
147+
const isBlockedRun = (
148+
trace: TraceEvent[],
149+
approvalData: Record<string, unknown>,
150+
): boolean => {
151+
if (approvalData["outcome"] === "blocked") return true;
152+
const matching = trace.find(
153+
(e) => e.stage === "matching" && e.kind === "step",
154+
);
155+
return isRecord(matching?.data) && matching.data["verdict"] === "duplicate";
156+
};
157+
134158
/** Build the approval engine's InvoiceContext from the matching trace event, so the
135159
run graph's path can be resolved client-side. Returns undefined (draw the full
136160
graph) if the matching event isn't present/valid yet.

0 commit comments

Comments
 (0)