jev-devtools 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +50 -6
  2. package/lib/flow/index.d.ts +143 -0
  3. package/lib/flow/index.js +1029 -0
  4. package/lib/flow/runtime.d.ts +5 -0
  5. package/lib/flow/runtime.js +81 -0
  6. package/lib/server/flows.d.ts +28 -0
  7. package/lib/server/flows.js +187 -0
  8. package/lib/server/index.d.ts +14 -1
  9. package/lib/server/index.js +100 -1
  10. package/lib/server/recorder.d.ts +4 -1
  11. package/lib/server/recorder.js +19 -3
  12. package/lib/src/ChoiceProbabilities.js +1 -1
  13. package/lib/src/ConnectedInspector.js +13 -4
  14. package/lib/src/DiagramContent.d.ts +6 -0
  15. package/lib/src/DiagramContent.js +32 -0
  16. package/lib/src/FlowDetailsPanel.d.ts +8 -0
  17. package/lib/src/FlowDetailsPanel.js +79 -0
  18. package/lib/src/FlowOverview.d.ts +8 -0
  19. package/lib/src/FlowOverview.js +440 -0
  20. package/lib/src/JevDevtools.d.ts +6 -1
  21. package/lib/src/JevDevtools.js +18 -4
  22. package/lib/src/TraceDiagram.d.ts +4 -1
  23. package/lib/src/TraceDiagram.js +30 -12
  24. package/lib/src/condition-inputs.d.ts +9 -0
  25. package/lib/src/condition-inputs.js +109 -0
  26. package/lib/src/flow-condition.d.ts +4 -0
  27. package/lib/src/flow-condition.js +48 -0
  28. package/lib/src/flow-model.d.ts +7 -2
  29. package/lib/src/flow-model.js +2 -11
  30. package/lib/src/flow-protocol.d.ts +93 -0
  31. package/lib/src/flow-protocol.js +1 -0
  32. package/lib/src/flow-stream.d.ts +8 -0
  33. package/lib/src/flow-stream.js +80 -0
  34. package/lib/src/inspector-styles.js +1 -1
  35. package/lib/src/inspector.css +746 -8
  36. package/lib/src/protocol.d.ts +5 -0
  37. package/lib/src/useDiagramViewport.d.ts +5 -1
  38. package/lib/src/useDiagramViewport.js +31 -9
  39. package/package.json +11 -4
  40. package/skills/setup-jev-devtools/SKILL.md +8 -4
  41. package/skills/setup-jev-devtools/references/flows.md +227 -0
  42. package/skills/setup-jev-devtools/references/nextjs.md +3 -1
  43. package/skills/setup-jev-devtools/references/react-node.md +2 -2
  44. package/skills/setup-jev-devtools/references/verify.md +1 -1
@@ -1,7 +1,11 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { emptyFlows } from "./flow-stream.js";
2
3
  import { lazy, memo, startTransition, Suspense, useMemo, useState, } from "react";
3
4
  import { TraceDiagram } from "./TraceDiagram.js";
4
5
  import { JsonBlock, findImage } from "./TraceContent.js";
6
+ const FlowOverview = lazy(() => import("./FlowOverview.js").then((module) => ({
7
+ default: module.FlowOverview,
8
+ })));
5
9
  let questionsLoaded = false;
6
10
  const Questions = lazy(() => import("./Questions.js").then((module) => {
7
11
  questionsLoaded = true;
@@ -13,7 +17,7 @@ const ms = (value) => value === undefined ? "—" : `${Math.round(value)} ms`;
13
17
  const RequestRow = memo(function RequestRow({ trace, selected, onSelect, }) {
14
18
  return (_jsxs("button", { className: `request-row ${selected ? "selected" : ""}`, onClick: () => onSelect(trace.id), children: [_jsx("span", { className: `status-dot ${trace.status}` }), _jsxs("span", { children: [_jsx("strong", { children: trace.label }), _jsxs("small", { children: [new Date(trace.startedAt).toLocaleTimeString(), " \u00B7 ", trace.source, " \u00B7", " ", ms(trace.durationMs)] })] }), _jsx("span", { className: "row-status", children: trace.status })] }));
15
19
  });
16
- const RequestDetail = memo(function RequestDetail({ trace, }) {
20
+ export const RequestDetail = memo(function RequestDetail({ trace, }) {
17
21
  // Keep tab selection across request switches and clear, as before. Local tab
18
22
  // changes no longer rerender the history list.
19
23
  const [tab, setTab] = useState("Flow");
@@ -31,7 +35,11 @@ const RequestDetail = memo(function RequestDetail({ trace, }) {
31
35
  setTab(name);
32
36
  }, children: name }, name))) }), _jsx("div", { className: `detail-body ${tab === "Flow" ? "detail-body-flow" : tab === "Questions" ? "detail-body-questions" : ""}`, children: _jsxs(Suspense, { fallback: _jsx("p", { className: "muted", role: "status", children: "Loading questions\u2026" }), children: [trace.error && (_jsxs("div", { role: "alert", className: "error-banner", children: [_jsx("strong", { children: trace.error.name }), " ", trace.error.message] })), trace.captureWarnings.map((w) => (_jsx("p", { className: "warning", children: w }, w))), tab === "Flow" && _jsx(TraceDiagram, { trace: trace }, trace.id), tab === "Questions" && (_jsx(Questions, { trace: trace }, trace.id)), tab === "Context" && (_jsxs(_Fragment, { children: [_jsx("p", { className: "muted", children: "Captured before dispatch. Known credential fields and email are redacted; captures are size limited." }), findImage(trace.request.state) && (_jsx("img", { className: "context-image", src: findImage(trace.request.state), alt: "Captured context" })), _jsx(JsonBlock, { value: trace.request.state }), trace.request.assets && (_jsx(JsonBlock, { value: trace.request.assets }))] })), tab === "Transport" && (_jsxs(_Fragment, { children: [_jsx("h3", { children: "HTTP attempts" }), _jsx("p", { className: "muted", children: "Attempt duration ends at response headers. Total duration includes SDK processing and retry waits." }), _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Attempt" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Duration" })] }) }), _jsx("tbody", { children: trace.attempts.map((a) => (_jsxs("tr", { children: [_jsx("td", { children: a.number }), _jsx("td", { children: a.error || a.status || "Pending" }), _jsx("td", { children: ms(a.durationMs) })] }, a.number))) })] }), !trace.attempts.length && _jsx("p", { children: "No HTTP attempt recorded." })] })), tab === "Raw" && _jsx(JsonBlock, { value: trace })] }) })] })) : (_jsxs("div", { className: "empty-detail", children: [_jsx("span", { className: "empty-symbol", children: "\u21B3" }), _jsx("h2", { children: "Follow a Jev request" }), _jsxs("p", { children: ["Context, parallel questions, typed answers.", _jsx("br", {}), "Run a sample above to see the complete flow."] })] })) }));
33
37
  });
34
- export function JevPanel({ traces, connection, onClear, }) {
38
+ export function JevPanel({ traces, flows = emptyFlows, connection, onClear, }) {
39
+ const [view, setView] = useState("requests");
40
+ const [selectedRun, setSelectedRun] = useState();
41
+ const run = flows.runs.find((r) => r.id === selectedRun) ?? flows.runs[0];
42
+ const definition = flows.definitions.find((d) => d.id === run?.definitionId);
35
43
  const [selected, setSelected] = useState();
36
44
  const [search, setSearch] = useState("");
37
45
  const [status, setStatus] = useState("all");
@@ -42,7 +50,13 @@ export function JevPanel({ traces, connection, onClear, }) {
42
50
  `${t.label} ${t.request.model} ${t.id}`.toLowerCase().includes(query));
43
51
  }, [traces, search, status]);
44
52
  const trace = filtered.find((t) => t.id === selected) || filtered[0];
45
- return (_jsxs("div", { className: "jev-inspector", children: [_jsxs("aside", { className: "request-list", children: [_jsxs("header", { children: [_jsxs("h2", { children: ["Jev requests ", _jsx("span", { children: traces.length })] }), _jsx("button", { onClick: () => void onClear().catch((e) => setError(e.message)), disabled: !traces.length, children: "Clear" })] }), _jsxs("div", { className: "list-filters", children: [_jsx("input", { "aria-label": "Search requests", placeholder: "Search requests\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), _jsx("select", { "aria-label": "Filter request status", value: status, onChange: (e) => setStatus(e.target.value), children: ["all", "pending", "success", "error", "cancelled"].map((s) => (_jsx("option", { children: s }, s))) })] }), _jsxs("div", { className: "request-items", children: [filtered.map((t) => (_jsx(RequestRow, { trace: t, selected: trace?.id === t.id, onSelect: setSelected }, t.id))), !filtered.length && (_jsx("p", { className: "empty", children: traces.length
53
+ return (_jsxs("div", { className: "jev-inspector", children: [_jsxs("aside", { className: "request-list", children: [_jsxs("header", { children: [_jsxs("h2", { children: [view === "requests" ? "Jev requests" : "Jev flows", " ", _jsx("span", { children: view === "requests" ? traces.length : flows.runs.length })] }), _jsx("button", { onClick: () => void onClear().catch((e) => setError(e.message)), disabled: !traces.length && !flows.runs.length, children: "Clear" })] }), (flows.runs.length > 0 || view === "flows") && (_jsxs("nav", { className: "detail-tabs", "aria-label": "History type", children: [_jsx("button", { "aria-pressed": view === "requests", onClick: () => setView("requests"), children: "Requests" }), _jsx("button", { "aria-pressed": view === "flows", onClick: () => {
54
+ setView("flows");
55
+ if (run)
56
+ setSelectedRun(run.id);
57
+ }, children: "Flows" })] })), view === "requests" && (_jsxs("div", { className: "list-filters", children: [_jsx("input", { "aria-label": "Search requests", placeholder: "Search requests\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), _jsx("select", { "aria-label": "Filter request status", value: status, onChange: (e) => setStatus(e.target.value), children: ["all", "pending", "success", "error", "cancelled"].map((s) => (_jsx("option", { children: s }, s))) })] })), _jsxs("div", { className: "request-items", children: [view === "flows"
58
+ ? flows.runs.map((r) => (_jsxs("button", { className: `request-row ${run?.id === r.id ? "selected" : ""}`, onClick: () => setSelectedRun(r.id), children: [_jsx("span", { className: `status-dot ${r.status}` }), _jsxs("span", { children: [_jsx("strong", { children: r.name }), _jsxs("small", { children: [new Date(r.startedAt).toLocaleTimeString(), " \u00B7", " ", r.outcome ?? r.status] })] }), _jsx("span", { className: "row-status", children: r.status })] }, r.id)))
59
+ : filtered.map((t) => (_jsx(RequestRow, { trace: t, selected: trace?.id === t.id, onSelect: setSelected }, t.id))), view === "requests" && !filtered.length && (_jsx("p", { className: "empty", children: traces.length
46
60
  ? "No matching requests."
47
- : "Run a scenario to capture your first request." }))] }), (connection !== "connected" || error) && (_jsxs("footer", { children: [connection !== "connected" && (_jsx("span", { role: "status", children: connection })), error && _jsx("p", { role: "alert", children: error })] }))] }), _jsx(RequestDetail, { trace: trace })] }));
61
+ : "Run a scenario to capture your first request." }))] }), (connection !== "connected" || error) && (_jsxs("footer", { children: [connection !== "connected" && (_jsx("span", { role: "status", children: connection })), error && _jsx("p", { role: "alert", children: error })] }))] }), view === "flows" && run && definition ? (_jsx(Suspense, { fallback: _jsx("p", { role: "status", children: "Loading flow\u2026" }), children: _jsx(FlowOverview, { run: run, definition: definition, traces: traces }, run.id) })) : (_jsx(RequestDetail, { trace: trace }))] }));
48
62
  }
@@ -1,4 +1,7 @@
1
1
  import type { Trace } from "./protocol.js";
2
- export declare function TraceDiagram({ trace }: {
2
+ export declare function TraceDiagram({ trace, embedded, onHeightChange, }: {
3
3
  trace: Trace;
4
+ /** Render in a parent diagram's coordinates, without a nested camera. */
5
+ embedded?: boolean;
6
+ onHeightChange?: (height: number) => void;
4
7
  }): import("react").JSX.Element;
@@ -1,7 +1,8 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useState } from "react";
2
+ import { useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import { useDiagramViewport } from "./useDiagramViewport.js";
4
- import { findImage, flowAnswer, stateText } from "./flow-model.js";
4
+ import { findImage, flowAnswer } from "./flow-model.js";
5
+ import { DiagramContent } from "./DiagramContent.js";
5
6
  import { ChoiceProbabilities } from "./ChoiceProbabilities.js";
6
7
  const pct = (v) => `${Math.round(v * 100)}%`;
7
8
  function Result({ answer, image, probabilities, }) {
@@ -24,7 +25,20 @@ function Result({ answer, image, probabilities, }) {
24
25
  height: pct(answer.box.bottom - answer.box.top),
25
26
  } }))] }), _jsxs("p", { className: "lab-small lab-coordinates", children: ["left ", answer.box.left.toFixed(2), " \u00B7 top ", answer.box.top.toFixed(2), _jsx("br", {}), "right ", answer.box.right.toFixed(2), " \u00B7 bottom", " ", answer.box.bottom.toFixed(2)] })] })), answer.confidence !== undefined && (_jsxs("p", { className: "lab-small", children: ["Confidence ", pct(answer.confidence)] }))] }));
26
27
  }
27
- export function TraceDiagram({ trace }) {
28
+ export function TraceDiagram({ trace, embedded = false, onHeightChange, }) {
29
+ const root = useRef(null);
30
+ useLayoutEffect(() => {
31
+ if (!embedded || !root.current || !onHeightChange)
32
+ return;
33
+ const element = root.current;
34
+ const observer = new ResizeObserver(() => {
35
+ // Closed drawers and skipped offscreen content cannot replace retained bounds.
36
+ if (element.offsetHeight > 0)
37
+ onHeightChange(element.offsetHeight);
38
+ });
39
+ observer.observe(element);
40
+ return () => observer.disconnect();
41
+ }, [embedded, onHeightChange]);
28
42
  const [selected, setSelected] = useState(""), [flow, setFlow] = useState(true), [extraCategories, setExtraCategories] = useState({}), [entering, setEntering] = useState(null), [probabilityView, setProbabilityView] = useState(null);
29
43
  const entries = Object.entries(trace.request.questions ?? {}).filter(([, q]) => q && typeof q === "object" && typeof q.type === "string"), [key, question] = entries.find(([key]) => key === selected) ??
30
44
  entries[0] ?? ["", undefined];
@@ -79,7 +93,7 @@ export function TraceDiagram({ trace }) {
79
93
  active: output.active,
80
94
  })),
81
95
  ];
82
- return (_jsxs("div", { className: `lab-flow ${compact ? "lab-flow-compact" : ""}`, children: [_jsxs("div", { className: "lab-flow-toolbar", children: [_jsxs("label", { children: ["Question", _jsx("select", { "aria-label": "Diagram question", value: key, onChange: (event) => {
96
+ return (_jsxs("div", { ref: root, className: `lab-flow ${compact ? "lab-flow-compact" : ""} ${embedded ? "lab-flow-embedded" : ""}`, children: [_jsxs("div", { className: "lab-flow-toolbar", children: [_jsxs("label", { children: ["Question", _jsx("select", { "aria-label": "Diagram question", value: key, onChange: (event) => {
83
97
  setEntering(null);
84
98
  setSelected(event.target.value);
85
99
  }, children: entries.map(([name, q]) => (_jsxs("option", { value: name, children: [name, " \u00B7 ", q.type] }, name))) })] }), _jsxs("div", { children: [answer?.type === "choice" && (_jsx("button", { "aria-pressed": showProbabilities, onClick: () => {
@@ -91,21 +105,19 @@ export function TraceDiagram({ trace }) {
91
105
  preserveCamera();
92
106
  setEntering(null);
93
107
  setExtraCategories((counts) => ({ ...counts, [key]: 0 }));
94
- }, children: "Show fewer" })), _jsx("button", { "aria-label": "Zoom diagram out", disabled: zoom <= 10, onClick: () => zoomBy(1 / 1.2), children: "\u2212" }), _jsxs("output", { "aria-label": "Diagram zoom", children: [zoom, "%"] }), _jsx("button", { "aria-label": "Zoom diagram in", disabled: zoom >= 400, onClick: () => zoomBy(1.2), children: "+" }), _jsx("button", { onClick: fit, children: "Fit" }), _jsx("button", { "aria-pressed": flow, onClick: () => setFlow(!flow), children: flow ? "Pause flow" : "Play flow" })] }))] })] }), showProbabilities && answer && (_jsx(ChoiceProbabilities, { answer: answer }, key)), _jsx("div", { hidden: showProbabilities, className: "lab-flow-viewport", ref: viewport, tabIndex: 0, role: "group", "aria-label": "Jev diagram. Drag to pan, scroll or pinch to zoom. Arrow keys pan, plus and minus zoom, zero fits.", children: _jsxs("div", { ref: canvas, className: "lab-flow-canvas", style: { width: 1120, height }, children: [_jsxs("svg", { className: "lab-connections", width: "1120", height: height, "aria-hidden": "true", children: [paths.map((path) => (_jsxs("g", { children: [_jsx("path", { d: path.d, pathLength: 1, className: `${path.active ? "active" : ""} ${"delay" in path && path.delay !== undefined ? "lab-branch-enter" : ""}`, style: {
108
+ }, children: "Show fewer" })), !embedded && (_jsxs(_Fragment, { children: [_jsx("button", { "aria-label": "Zoom diagram out", disabled: zoom <= 10, onClick: () => zoomBy(1 / 1.2), children: "\u2212" }), _jsxs("output", { "aria-label": "Diagram zoom", children: [zoom, "%"] }), _jsx("button", { "aria-label": "Zoom diagram in", disabled: zoom >= 400, onClick: () => zoomBy(1.2), children: "+" }), _jsx("button", { onClick: fit, children: "Fit" })] })), _jsx("button", { "aria-pressed": flow, onClick: () => setFlow(!flow), children: flow ? "Pause flow" : "Play flow" })] }))] })] }), showProbabilities && answer && (_jsx(ChoiceProbabilities, { answer: answer }, key)), _jsx("div", { hidden: showProbabilities, className: embedded ? "lab-flow-surface" : "lab-flow-viewport", ref: embedded ? undefined : viewport, style: embedded ? { height } : undefined, tabIndex: embedded ? undefined : 0, role: "group", "aria-label": embedded
109
+ ? "Jev request diagram"
110
+ : "Jev diagram. Drag to pan, scroll or pinch to zoom. Arrow keys pan, plus and minus zoom, zero fits.", children: _jsxs("div", { ref: embedded ? undefined : canvas, className: "lab-flow-canvas", style: { width: 1120, height }, children: [_jsxs("svg", { className: "lab-connections", width: "1120", height: height, "aria-hidden": "true", children: [paths.map((path) => (_jsxs("g", { children: [_jsx("path", { d: path.d, pathLength: 1, className: `${path.active ? "active" : ""} ${"delay" in path && path.delay !== undefined ? "lab-branch-enter" : ""}`, style: {
95
111
  animationDelay: "delay" in path ? path.delay : undefined,
96
112
  } }), flow &&
97
113
  !showProbabilities &&
98
114
  path.active &&
99
- !["error", "cancelled"].includes(trace.status) && (_jsx("circle", { r: "3", className: "lab-flow-dot", children: _jsx("animateMotion", { dur: "3.2s", repeatCount: "indefinite", path: path.d }) }))] }, path.id))), _jsx("text", { x: "305", y: mid - 65, children: "state" }), _jsx("text", { x: "300", y: mid + 65, children: "question" })] }), _jsxs("article", { className: "lab-card", style: { left: 20, top: 32, width: 240, height: 180 }, children: [_jsx("span", { className: "lab-eyebrow", children: image ? "IMAGE STATE" : "STATE" }), _jsx("h3", { children: "Shared context" }), image ? (_jsx("img", { className: "lab-source-image", src: image, alt: "Captured image state" })) : (_jsx("p", { className: "lab-copy", title: stateText(trace), children: stateText(trace) }))] }), _jsxs("article", { className: "lab-card", style: { left: 20, top: 288, width: 240, height: 180 }, children: [_jsx("span", { className: "lab-eyebrow", children: "INSTRUCTIONS" }), _jsxs("h3", { children: ["Question \u00B7 ", question.type] }), _jsx("p", { className: "lab-copy", title: typeof question.instructions === "string"
100
- ? question.instructions
101
- : JSON.stringify(question.instructions), children: typeof question.instructions === "string"
102
- ? question.instructions
103
- : JSON.stringify(question.instructions) })] }), _jsxs("article", { className: "lab-card lab-answer-card", style: {
115
+ !["error", "cancelled"].includes(trace.status) && (_jsx("circle", { r: "3", className: "lab-flow-dot", children: _jsx("animateMotion", { dur: "3.2s", repeatCount: "indefinite", path: path.d }) }))] }, path.id))), _jsx("text", { x: "305", y: mid - 65, children: "state" }), _jsx("text", { x: "300", y: mid + 65, children: "question" })] }), _jsxs("article", { className: "lab-card lab-content-card", style: { left: 20, top: 32, width: 240, height: 180 }, children: [_jsx("span", { className: "lab-eyebrow", children: image ? "IMAGE STATE" : "STATE" }), _jsx("h3", { children: "Shared context" }), image ? (_jsx("img", { className: "lab-source-image", src: image, alt: "Captured image state" })) : (_jsx(DiagramContent, { value: trace.request.state, label: "State properties" }, trace.id))] }), _jsxs("article", { className: "lab-card lab-content-card", style: { left: 20, top: 288, width: 240, height: 180 }, children: [_jsx("span", { className: "lab-eyebrow", children: "INSTRUCTIONS" }), _jsxs("h3", { children: ["Question \u00B7 ", question.type] }), _jsx(DiagramContent, { value: question.instructions, label: "Question instructions" }, `${trace.id}:${key}`)] }), _jsxs("article", { className: "lab-card lab-answer-card", style: {
104
116
  left: 390,
105
117
  top: ry,
106
118
  width: 320,
107
119
  minHeight: resultHeight,
108
- }, children: [_jsxs("span", { className: "lab-eyebrow", children: ["JEV \u00B7 ", question.type.toUpperCase()] }), _jsx("h3", { title: key, children: key }), compact && (_jsxs("p", { className: "lab-small", children: ["Showing ", shownProbabilities.length, " of ", allProbabilities.length, " ", "probabilities"] })), _jsx(Result, { answer: answer, image: image, probabilities: compact ? shownProbabilities : undefined })] }), outputs.map((output, i) => (_jsxs("article", { className: `lab-card lab-output ${output.active ? "" : "lab-idle"} ${enteringDelay(i) !== undefined ? "lab-card-enter" : ""}`, onAnimationEnd: () => {
120
+ }, children: [_jsxs("span", { className: "lab-eyebrow", children: ["JEV \u00B7 ", question.type.toUpperCase()] }), _jsx("h3", { title: key, children: key }), compact && (_jsxs("p", { className: "lab-small", children: ["Showing ", shownProbabilities.length, " of ", allProbabilities.length, " ", "probabilities"] })), _jsx(Result, { answer: answer, image: image, probabilities: compact ? shownProbabilities : undefined })] }), outputs.map((output, i) => (_jsxs("article", { className: `lab-card lab-output ${"probability" in output ? "lab-noul-output" : ""} ${output.active ? "" : "lab-idle"} ${enteringDelay(i) !== undefined ? "lab-card-enter" : ""}`, onAnimationEnd: () => {
109
121
  if (entering?.question === key && i === entering.to - 1)
110
122
  setEntering(null);
111
123
  }, style: {
@@ -114,6 +126,12 @@ export function TraceDiagram({ trace }) {
114
126
  width: 230,
115
127
  minHeight: compact ? 96 : 104,
116
128
  animationDelay: enteringDelay(i),
129
+ ...("probability" in output &&
130
+ Number.isFinite(output.probability)
131
+ ? {
132
+ "--lab-noul-probability": Math.max(0, Math.min(1, output.probability)),
133
+ }
134
+ : {}),
117
135
  }, children: [_jsx("span", { className: "lab-eyebrow", children: output.eyebrow }), _jsx("h3", { title: output.label, children: output.label }), _jsx("p", { children: output.detail })] }, `${key}-${output.label}`))), remaining > 0 && (_jsxs("article", { className: "lab-card lab-choice-remainder", style: {
118
136
  left: 870,
119
137
  top: 40 + outputs.length * spacing,
@@ -130,5 +148,5 @@ export function TraceDiagram({ trace }) {
130
148
  ...counts,
131
149
  [key]: (counts[key] ?? 0) + 3,
132
150
  }));
133
- }, children: "Show More" }), _jsxs("p", { className: "lab-small", children: [remaining, " remaining \u00B7 Add ", Math.min(3, remaining)] })] }))] }) }), !showProbabilities && (_jsxs("p", { className: "muted lab-flow-caption", children: [_jsx("span", { className: "lab-gesture-hint", children: "Drag to pan \u00B7 Scroll or pinch to zoom \u00B7 0 to fit" }), entries.length, " parallel question", entries.length === 1 ? "" : "s", " in one request. Connections show data flow, not model timing. Application routes and thresholds are not captured. Full inputs and criteria are in Questions and Context."] }))] }));
151
+ }, children: "Show More" }), _jsxs("p", { className: "lab-small", children: [remaining, " remaining \u00B7 Add ", Math.min(3, remaining)] })] }))] }) }), !showProbabilities && !embedded && (_jsxs("p", { className: "muted lab-flow-caption", children: [_jsx("span", { className: "lab-gesture-hint", children: "Drag to pan \u00B7 Scroll or pinch to zoom \u00B7 0 to fit" }), entries.length, " parallel question", entries.length === 1 ? "" : "s", " in one request. Connections show data flow, not model timing. Application routes and thresholds are not captured. Full inputs and criteria are in Questions and Context."] }))] }));
134
152
  }
@@ -0,0 +1,9 @@
1
+ import type { Json, Trace } from "./protocol.js";
2
+ import type { FlowRun } from "./flow-protocol.js";
3
+ export type ConditionInput = {
4
+ reference: string;
5
+ name: string;
6
+ value: string;
7
+ };
8
+ export declare function conditionInputs(rule: Json, run: FlowRun, traces: Map<string, Trace>, stepNames?: Map<string, string>): ConditionInput[];
9
+ export declare function conditionRequirement(rule: Json, inputs: ConditionInput[]): string;
@@ -0,0 +1,109 @@
1
+ import { formatFlowCondition, formatConditionLiteral, } from "./flow-condition.js";
2
+ export function conditionInputs(rule, run, traces, stepNames = new Map()) {
3
+ const inputs = new Map();
4
+ const sources = new Map();
5
+ const visit = (node, depth = 0) => {
6
+ if (depth > 16 || !node || typeof node !== "object" || Array.isArray(node))
7
+ return;
8
+ if (node.op === "reference" &&
9
+ typeof node.label === "string" &&
10
+ !inputs.has(node.label)) {
11
+ const name = typeof node.name === "string" ? node.name : "Input";
12
+ const readable = name
13
+ .replace(/[_-]+/g, " ")
14
+ .replace(/([a-z])([A-Z])/g, "$1 $2");
15
+ const trace = typeof node.stepId === "string"
16
+ ? traces.get(run.steps[node.stepId]?.traceId ?? "")
17
+ : undefined;
18
+ let value = trace?.result;
19
+ let available = !!trace?.result;
20
+ if (node.source === "task" && typeof node.stepId === "string") {
21
+ const data = run.steps[node.stepId]?.data;
22
+ available =
23
+ !!data &&
24
+ typeof data === "object" &&
25
+ !Array.isArray(data) &&
26
+ Object.hasOwn(data, "output");
27
+ value = available ? data.output : undefined;
28
+ }
29
+ if (!Array.isArray(node.path) ||
30
+ !node.path.every((key) => typeof key === "string"))
31
+ available = false;
32
+ else
33
+ for (const key of node.path) {
34
+ if (!available ||
35
+ !value ||
36
+ typeof value !== "object" ||
37
+ !Object.hasOwn(value, String(key))) {
38
+ available = false;
39
+ break;
40
+ }
41
+ value = value[String(key)];
42
+ }
43
+ const text = !available
44
+ ? "Not captured"
45
+ : node.probability === true &&
46
+ typeof value === "number" &&
47
+ Number.isFinite(value)
48
+ ? formatConditionLiteral(value, true)
49
+ : typeof value === "object" && value !== null
50
+ ? "Structured value"
51
+ : String(value);
52
+ if (typeof node.stepId === "string")
53
+ sources.set(node.label, stepNames.get(node.stepId) ?? node.stepId);
54
+ inputs.set(node.label, {
55
+ reference: node.label,
56
+ name: readable.charAt(0).toUpperCase() + readable.slice(1),
57
+ value: text,
58
+ });
59
+ }
60
+ if (Array.isArray(node.args))
61
+ for (const arg of node.args)
62
+ visit(arg, depth + 1);
63
+ };
64
+ visit(rule);
65
+ const list = [...inputs.values()];
66
+ return list.map((input) => list.filter((other) => other.name === input.name).length > 1
67
+ ? {
68
+ ...input,
69
+ name: `${input.name} · ${sources.get(input.reference) ?? input.reference}`,
70
+ }
71
+ : input);
72
+ }
73
+ export function conditionRequirement(rule, inputs) {
74
+ const names = new Map(inputs.map((input) => [input.reference, input.name]));
75
+ // A single reference-to-literal comparison already has its subject in Incoming.
76
+ if (rule &&
77
+ typeof rule === "object" &&
78
+ !Array.isArray(rule) &&
79
+ Array.isArray(rule.args) &&
80
+ rule.args.length === 2) {
81
+ const [left, right] = rule.args;
82
+ if (left &&
83
+ right &&
84
+ typeof left === "object" &&
85
+ typeof right === "object" &&
86
+ !Array.isArray(left) &&
87
+ !Array.isArray(right) &&
88
+ left.op === "reference" &&
89
+ right.op === "literal" &&
90
+ ["gte", "lt", "gt", "eq"].includes(String(rule.op))) {
91
+ return `${rule.op === "gte" ? "At least" : rule.op === "lt" ? "Less than" : rule.op === "gt" ? "Greater than" : "Equal to"} ${formatConditionLiteral(right.value, left.probability === true)}`;
92
+ }
93
+ }
94
+ const rename = (node, depth = 0) => {
95
+ if (depth > 16 || !node || typeof node !== "object" || Array.isArray(node))
96
+ return node;
97
+ if (node.op === "reference")
98
+ return {
99
+ ...node,
100
+ label: typeof node.label === "string"
101
+ ? names.get(node.label) ?? "Input"
102
+ : "Input",
103
+ };
104
+ if (Array.isArray(node.args))
105
+ return { ...node, args: node.args.map((arg) => rename(arg, depth + 1)) };
106
+ return node;
107
+ };
108
+ return formatFlowCondition(rename(rule));
109
+ }
@@ -0,0 +1,4 @@
1
+ import type { Json } from "./protocol.js";
2
+ export declare function formatConditionLiteral(value: Json, probability?: boolean): string;
3
+ /** Format captured rules only; never evaluate or infer the branch result. */
4
+ export declare function formatFlowCondition(value: Json, depth?: number): string;
@@ -0,0 +1,48 @@
1
+ export function formatConditionLiteral(value, probability = false) {
2
+ if (probability && typeof value === "number" && value >= 0 && value <= 1) {
3
+ const [coefficient, exponent = "0"] = String(value).split("e");
4
+ return `${Number(`${coefficient}e${Number(exponent) + 2}`)}%`;
5
+ }
6
+ return JSON.stringify(value) ?? "[value unavailable]";
7
+ }
8
+ /** Format captured rules only; never evaluate or infer the branch result. */
9
+ export function formatFlowCondition(value, depth = 0) {
10
+ if (depth > 16)
11
+ return "[rule truncated]";
12
+ if (!value || typeof value !== "object" || Array.isArray(value))
13
+ return typeof value === "string" ? value : "[rule unavailable]";
14
+ if (value.op === "reference")
15
+ return typeof value.label === "string"
16
+ ? value.label
17
+ : "[reference unavailable]";
18
+ if (value.op === "literal")
19
+ return formatConditionLiteral(value.value);
20
+ if (!Array.isArray(value.args))
21
+ return "[rule unavailable]";
22
+ const args = value.args;
23
+ const format = (v) => formatFlowCondition(v, depth + 1);
24
+ if (value.op === "not")
25
+ return `NOT (${format(args[0])})`;
26
+ if (value.op === "all" || value.op === "any") {
27
+ if (!args.length)
28
+ return value.op === "all"
29
+ ? "TRUE (no conditions)"
30
+ : "FALSE (no conditions)";
31
+ return `(${args.map(format).join(value.op === "all" ? " AND " : " OR ")})`;
32
+ }
33
+ if (!["eq", "lt", "gt", "gte"].includes(String(value.op)) ||
34
+ args.length !== 2)
35
+ return "[rule unavailable]";
36
+ const probability = args.some((arg) => arg &&
37
+ typeof arg === "object" &&
38
+ !Array.isArray(arg) &&
39
+ arg.op === "reference" &&
40
+ arg.probability === true);
41
+ const operand = (arg) => arg &&
42
+ typeof arg === "object" &&
43
+ !Array.isArray(arg) &&
44
+ arg.op === "literal"
45
+ ? formatConditionLiteral(arg.value, probability)
46
+ : format(arg);
47
+ return `${operand(args[0])} ${value.op === "eq" ? "=" : value.op === "lt" ? "<" : value.op === "gt" ? ">" : "≥"} ${operand(args[1])}`;
48
+ }
@@ -1,9 +1,14 @@
1
- import type { AnswerView, Json, Trace } from "./protocol.js";
1
+ import type { AnswerView, Json } from "./protocol.js";
2
2
  export declare function findImage(value: Json): string | undefined;
3
3
  export declare function flowAnswer(answer?: AnswerView): {
4
4
  label: string;
5
5
  detail: string;
6
6
  active: boolean;
7
7
  eyebrow: string;
8
+ }[] | {
9
+ label: string;
10
+ detail: string;
11
+ active: boolean;
12
+ eyebrow: string;
13
+ probability: number;
8
14
  }[];
9
- export declare function stateText(trace: Trace): string;
@@ -37,12 +37,14 @@ export function flowAnswer(answer) {
37
37
  detail: `${Math.round(answer.noul * 100)}% probability`,
38
38
  active: true,
39
39
  eyebrow: "P(TRUE)",
40
+ probability: answer.noul,
40
41
  },
41
42
  {
42
43
  label: "False",
43
44
  detail: `${Math.round((1 - answer.noul) * 100)}% probability`,
44
45
  active: true,
45
46
  eyebrow: "P(FALSE)",
47
+ probability: 1 - answer.noul,
46
48
  },
47
49
  ];
48
50
  if (answer.type === "box")
@@ -56,14 +58,3 @@ export function flowAnswer(answer) {
56
58
  ];
57
59
  return [];
58
60
  }
59
- export function stateText(trace) {
60
- const state = trace.request.state;
61
- if (typeof state === "string")
62
- return state;
63
- if (state && typeof state === "object" && !Array.isArray(state)) {
64
- for (const key of ["message", "text", "description"])
65
- if (typeof state[key] === "string")
66
- return state[key];
67
- }
68
- return JSON.stringify(state, null, 2) ?? "";
69
- }
@@ -0,0 +1,93 @@
1
+ import type { Json } from "./protocol.js";
2
+ /** Authored diagram copy, never instructions for Jev or executable routing logic. */
3
+ export type FlowDisplay = {
4
+ title?: string;
5
+ description?: string;
6
+ };
7
+ /** Rule literals are captured/redacted by the recorder before browser delivery. */
8
+ export type FlowCondition = {
9
+ op: "reference";
10
+ label: string;
11
+ probability: boolean;
12
+ name?: string;
13
+ stepId?: string;
14
+ path?: string[];
15
+ source?: "task";
16
+ } | {
17
+ op: "literal";
18
+ value: string | number | boolean | null;
19
+ } | {
20
+ op: "eq" | "lt" | "gt" | "gte" | "all" | "any" | "not";
21
+ args: FlowCondition[];
22
+ };
23
+ /** Structural metadata and authored rules; runtime inputs/answers stay in request captures. */
24
+ export type FlowNode = FlowDisplay & {
25
+ id: string;
26
+ kind: "jev" | "task" | "map" | "loop" | "if" | "merge" | "branch" | "parallel" | "output";
27
+ dependencies: string[];
28
+ path: string[];
29
+ condition?: string;
30
+ /** Conditional merge edges; only the selected arm supplies a value. */
31
+ merge?: {
32
+ condition: string;
33
+ then: string;
34
+ else: string;
35
+ };
36
+ /** Optional for older captures; Json allows redacted/truncated subtrees. */
37
+ conditionRule?: Json | FlowCondition;
38
+ /** Named concurrent lanes; optional for older captures. */
39
+ lanes?: string[];
40
+ /** Reusable item program; map nesting is not supported in v1. */
41
+ itemDefinition?: FlowDefinition;
42
+ };
43
+ export type FlowDefinition = {
44
+ version: 1;
45
+ id: string;
46
+ name: string;
47
+ nodes: FlowNode[];
48
+ };
49
+ export type FlowStep = {
50
+ id: string;
51
+ executionId: string;
52
+ status: "waiting" | "running" | "completed" | "untaken" | "blocked" | "error" | "cancelled";
53
+ startedAt?: number;
54
+ durationMs?: number;
55
+ decision?: boolean;
56
+ traceId?: string;
57
+ error?: string;
58
+ /** Optional bounded, redacted task input/output or selected merge value; never executable code. */
59
+ data?: Json;
60
+ captureWarnings?: string[];
61
+ loop?: {
62
+ maxIterations: number;
63
+ stopReason?: "condition" | "limit";
64
+ };
65
+ collection?: {
66
+ total: number;
67
+ concurrency: number;
68
+ items: Array<{
69
+ index: number;
70
+ status: FlowStep["status"];
71
+ run?: FlowRun;
72
+ }>;
73
+ };
74
+ };
75
+ export type FlowRun = {
76
+ version: 1;
77
+ id: string;
78
+ definitionId: string;
79
+ name: string;
80
+ startedAt: number;
81
+ durationMs?: number;
82
+ status: "pending" | "success" | "error" | "cancelled";
83
+ steps: Record<string, FlowStep>;
84
+ outcome?: string;
85
+ value?: Json;
86
+ error?: string;
87
+ };
88
+ export type FlowUpdate = {
89
+ version: 1;
90
+ definitions: FlowDefinition[];
91
+ upserts: FlowRun[];
92
+ order: string[];
93
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { FlowDefinition, FlowRun } from "./flow-protocol.js";
2
+ export type FlowHistory = {
3
+ runs: FlowRun[];
4
+ definitions: FlowDefinition[];
5
+ };
6
+ export declare const emptyFlows: FlowHistory;
7
+ /** Separate opt-in stream; no extra rendering of request rows on flow-only updates. */
8
+ export declare function connectFlowStream(url: string, onUpdate: (history: FlowHistory) => void): () => void;
@@ -0,0 +1,80 @@
1
+ export const emptyFlows = { runs: [], definitions: [] };
2
+ /** Separate opt-in stream; no extra rendering of request rows on flow-only updates. */
3
+ export function connectFlowStream(url, onUpdate) {
4
+ let alive = true, source, frame, fallback, retry;
5
+ let runs = new Map(), definitions = new Map(), pending;
6
+ const cancel = () => {
7
+ if (frame !== undefined)
8
+ cancelAnimationFrame(frame);
9
+ clearTimeout(fallback);
10
+ frame = undefined;
11
+ pending = undefined;
12
+ };
13
+ const publish = () => {
14
+ const next = pending;
15
+ cancel();
16
+ if (alive && next)
17
+ onUpdate(next);
18
+ };
19
+ const connect = () => {
20
+ if (!alive)
21
+ return;
22
+ runs = new Map();
23
+ definitions = new Map();
24
+ const current = new EventSource(url + "/flows/events");
25
+ source = current;
26
+ current.addEventListener("flows", (event) => {
27
+ if (!alive || source !== current)
28
+ return;
29
+ try {
30
+ const data = JSON.parse(event.data);
31
+ if (data.version !== 1 ||
32
+ !Array.isArray(data.order) ||
33
+ data.order.length > 200 ||
34
+ new Set(data.order).size !== data.order.length ||
35
+ !Array.isArray(data.upserts) ||
36
+ !Array.isArray(data.definitions))
37
+ throw new Error("Invalid flow stream");
38
+ for (const definition of data.definitions) {
39
+ if (definition.version !== 1 ||
40
+ !Array.isArray(definition.nodes) ||
41
+ definition.nodes.length > 256)
42
+ throw new Error("Invalid flow definition");
43
+ if (!definitions.has(definition.id))
44
+ definitions.set(definition.id, definition);
45
+ }
46
+ for (const run of data.upserts) {
47
+ if (run.version !== 1 || !definitions.has(run.definitionId))
48
+ throw new Error("Invalid flow run");
49
+ runs.set(run.id, run);
50
+ }
51
+ const ordered = data.order.map((id) => {
52
+ const run = runs.get(id);
53
+ if (!run)
54
+ throw new Error("Missing run");
55
+ return run;
56
+ });
57
+ runs = new Map(ordered.map((r) => [r.id, r]));
58
+ const used = new Set(ordered.map((r) => r.definitionId));
59
+ definitions = new Map([...definitions].filter(([id]) => used.has(id)));
60
+ pending = { runs: ordered, definitions: [...definitions.values()] };
61
+ if (frame === undefined) {
62
+ frame = requestAnimationFrame(publish);
63
+ fallback = setTimeout(publish, 100);
64
+ }
65
+ }
66
+ catch {
67
+ cancel();
68
+ current.close();
69
+ retry = setTimeout(connect, 1000);
70
+ }
71
+ });
72
+ };
73
+ connect();
74
+ return () => {
75
+ alive = false;
76
+ cancel();
77
+ clearTimeout(retry);
78
+ source?.close();
79
+ };
80
+ }