jev-devtools 0.5.4 → 0.5.5

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.
@@ -1,24 +1,26 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CaptureFields, PartialBox } from "./CaptureFields.js";
3
+ import { captureFieldUnavailable, captureQuestionEntries, unavailableCaptureFields, traceCaptureView, } from "./capture-view.js";
2
4
  import { useLayoutEffect, useMemo, useRef, useState } from "react";
3
5
  import { useDiagramViewport } from "./useDiagramViewport.js";
4
6
  import { findImage, flowAnswer } from "./flow-model.js";
5
7
  import { DiagramContent } from "./DiagramContent.js";
6
8
  import { ChoiceProbabilities } from "./ChoiceProbabilities.js";
7
9
  const pct = (v) => `${Math.round(v * 100)}%`;
8
- function Result({ answer, image, probabilities, }) {
10
+ function Result({ answer, image, probabilities, scaleAvailable, }) {
9
11
  if (!answer)
10
12
  return _jsx("p", { className: "lab-copy", children: "No answer received." });
11
13
  return (_jsxs(_Fragment, { children: [answer.probabilities &&
12
14
  (probabilities ?? Object.entries(answer.probabilities)).map(([key, value]) => (_jsxs("div", { className: "lab-probability", children: [_jsxs("div", { children: [_jsx("span", { title: typeof answer.legend?.[key] === "string"
13
15
  ? answer.legend[key]
14
16
  : key, children: answer.type === "score" && answer.legend?.[key]
15
- ? `${key} · ${answer.legend[key]}`
17
+ ? `${key} · ${typeof answer.legend[key] === "string" ? answer.legend[key] : JSON.stringify(answer.legend[key])}`
16
18
  : key }), _jsxs("code", { title: `Exact probability: ${value}`, children: [Number((value * 100).toPrecision(6)), "%"] })] }), _jsx("div", { className: "lab-track", children: _jsx("i", { className: answer.type === "choice" && answer.choice !== key
17
19
  ? "lab-alternative"
18
- : "", style: { width: pct(Math.max(0, Math.min(1, value))) } }) })] }, key))), answer.choice !== undefined && (_jsxs("p", { className: "lab-result", title: `Selected: ${answer.choice}`, children: ["Selected: ", answer.choice] })), answer.score !== undefined && (_jsxs(_Fragment, { children: [_jsxs("p", { className: "lab-result", children: ["Score ", answer.score.toFixed(2)] }), _jsxs("div", { className: "lab-scale", children: [_jsx("span", { style: {
20
+ : "", style: { width: pct(Math.max(0, Math.min(1, value))) } }) })] }, key))), answer.choice !== undefined && (_jsxs("p", { className: "lab-result", title: `Selected: ${answer.choice}`, children: ["Selected: ", answer.choice] })), answer.score !== undefined && (_jsxs(_Fragment, { children: [_jsxs("p", { className: "lab-result", children: ["Score ", answer.score.toFixed(2)] }), scaleAvailable ? (_jsxs("div", { className: "lab-scale", children: [_jsx("span", { style: {
19
21
  left: pct(Math.max(0, Math.min(1, answer.score /
20
22
  Math.max(1, Object.keys(answer.probabilities ?? {}).length - 1)))),
21
- } }), Object.keys(answer.probabilities ?? {}).map((k) => (_jsx("small", { children: k }, k)))] })] })), answer.noul !== undefined && (_jsxs(_Fragment, { children: [_jsx("strong", { className: "lab-big", children: pct(answer.noul) }), _jsx("p", { className: "lab-small", children: "probability of true" }), _jsx("div", { className: "lab-track", children: _jsx("i", { style: { width: pct(answer.noul) } }) }), _jsxs("div", { className: "lab-endpoints", children: [_jsx("span", { children: "False" }), _jsx("span", { children: "True" })] })] })), answer.box && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "lab-image", children: [image ? (_jsx("img", { src: image, alt: "Captured image with returned Box overlay" })) : (_jsx("p", { children: "Image preview unavailable" })), image && (_jsx("div", { className: "lab-box", style: {
23
+ } }), Object.keys(answer.probabilities ?? {}).map((k) => (_jsx("small", { children: k }, k)))] })) : (_jsx("p", { className: "muted", children: "Score scale unavailable in this capture." }))] })), answer.noul !== undefined && (_jsxs(_Fragment, { children: [_jsx("strong", { className: "lab-big", children: pct(answer.noul) }), _jsx("p", { className: "lab-small", children: "probability of true" }), _jsx("div", { className: "lab-track", children: _jsx("i", { style: { width: pct(answer.noul) } }) }), _jsxs("div", { className: "lab-endpoints", children: [_jsx("span", { children: "False" }), _jsx("span", { children: "True" })] })] })), answer.box && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "lab-image", children: [image ? (_jsx("img", { src: image, alt: "Captured image with returned Box overlay" })) : (_jsx("p", { children: "Image preview unavailable" })), image && (_jsx("div", { className: "lab-box", style: {
22
24
  left: pct(answer.box.left),
23
25
  top: pct(answer.box.top),
24
26
  width: pct(answer.box.right - answer.box.left),
@@ -26,6 +28,7 @@ function Result({ answer, image, probabilities, }) {
26
28
  } }))] }), _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)] }))] }));
27
29
  }
28
30
  export function TraceDiagram({ trace, embedded = false, onHeightChange, }) {
31
+ trace = traceCaptureView(trace);
29
32
  const root = useRef(null);
30
33
  useLayoutEffect(() => {
31
34
  if (!embedded || !root.current || !onHeightChange)
@@ -40,10 +43,18 @@ export function TraceDiagram({ trace, embedded = false, onHeightChange, }) {
40
43
  return () => observer.disconnect();
41
44
  }, [embedded, onHeightChange]);
42
45
  const [selected, setSelected] = useState(""), [flow, setFlow] = useState(true), [extraCategories, setExtraCategories] = useState({}), [entering, setEntering] = useState(null), [probabilityView, setProbabilityView] = useState(null);
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) ??
46
+ const entries = captureQuestionEntries(trace), [key, question] = entries.find(([key]) => key === selected) ??
44
47
  entries[0] ?? ["", undefined];
45
48
  const answer = trace.result?.answers[key], image = findImage(trace.request.state) ||
46
49
  (trace.request.assets ? findImage(trace.request.assets) : undefined), allProbabilities = useMemo(() => Object.entries(answer?.probabilities ?? {}).sort((a, b) => b[1] - a[1]), [answer?.probabilities]);
50
+ const probabilitiesUnavailable = captureFieldUnavailable(trace, [
51
+ "result",
52
+ "answers",
53
+ key,
54
+ "probabilities",
55
+ ]) ||
56
+ unavailableCaptureFields(trace, ["result", "answers", key, "probabilities"])
57
+ .length > 0;
47
58
  const compact = answer?.type === "choice" && allProbabilities.length > 6;
48
59
  const shownProbabilities = compact
49
60
  ? allProbabilities.slice(0, 3)
@@ -69,23 +80,39 @@ export function TraceDiagram({ trace, embedded = false, onHeightChange, }) {
69
80
  : 0;
70
81
  const spacing = compact ? 112 : 136;
71
82
  const showProbabilities = probabilityView === key && answer?.type === "choice";
72
- const resultHeight = Math.max(280, (compact ? 230 : 166) +
73
- (compact
74
- ? shownProbabilities.length
75
- : Object.keys(answer?.probabilities ?? {}).length) *
76
- 44 +
77
- (answer?.score !== undefined ? 50 : 0), answer?.box ? 390 : 0);
83
+ const unavailableCount = unavailableCaptureFields(trace, [
84
+ "result",
85
+ "answers",
86
+ key,
87
+ ]).length;
88
+ const resultHeight = unavailableCount * 44 +
89
+ Math.max(280, (compact ? 230 : 166) +
90
+ (compact
91
+ ? shownProbabilities.length
92
+ : Object.keys(answer?.probabilities ?? {}).length) *
93
+ 44 +
94
+ (answer?.score !== undefined ? 50 : 0), answer?.box ? 390 : 0);
78
95
  const height = Math.max(520, resultHeight + 90, (outputs.length + (remaining ? 1 : 0)) * spacing + 60), ry = compact ? 40 : Math.max(40, (height - resultHeight) / 2), mid = ry + resultHeight / 2;
79
96
  const { viewport, canvas, zoom, zoomBy, fit, preserveCamera } = useDiagramViewport(1120, height, key);
80
97
  if (!question)
81
- return _jsx("p", { className: "muted", children: "No questions available in this capture." });
98
+ return captureFieldUnavailable(trace, ["request", "questions"]) ? (_jsx(CaptureFields, { trace: trace, path: captureFieldUnavailable(trace, ["request"])
99
+ ? ["request"]
100
+ : ["request", "questions"] })) : (_jsx("p", { className: "muted", children: "No questions available in this capture." }));
82
101
  const sourcePath = (y) => `M260 ${y} C325 ${y},325 ${mid},390 ${mid}`;
83
102
  const enteringDelay = (index) => entering?.question === key && index >= entering.from && index < entering.to
84
103
  ? `${(index - entering.from) * 55}ms`
85
104
  : undefined;
86
105
  const paths = [
87
- { id: "state", d: sourcePath(122), active: true },
88
- { id: "question", d: sourcePath(378), active: true },
106
+ {
107
+ id: "state",
108
+ d: sourcePath(122),
109
+ active: !captureFieldUnavailable(trace, ["request", "state"]),
110
+ },
111
+ {
112
+ id: "question",
113
+ d: sourcePath(378),
114
+ active: !captureFieldUnavailable(trace, ["request", "questions", key]),
115
+ },
89
116
  ...outputs.map((output, i) => ({
90
117
  id: `output-${key}-${output.label}`,
91
118
  delay: enteringDelay(i),
@@ -101,23 +128,23 @@ export function TraceDiagram({ trace, embedded = false, onHeightChange, }) {
101
128
  setProbabilityView(showProbabilities ? null : key);
102
129
  }, children: showProbabilities
103
130
  ? "Back to overview"
104
- : `All probabilities (${allProbabilities.length})` })), !showProbabilities && (_jsxs(_Fragment, { children: [compact && (_jsx("button", { disabled: (extraCategories[key] ?? 0) === 0, onClick: () => {
131
+ : `${probabilitiesUnavailable ? "Available" : "All"} probabilities (${allProbabilities.length})` })), !showProbabilities && (_jsxs(_Fragment, { children: [compact && (_jsx("button", { disabled: (extraCategories[key] ?? 0) === 0, onClick: () => {
105
132
  preserveCamera();
106
133
  setEntering(null);
107
134
  setExtraCategories((counts) => ({ ...counts, [key]: 0 }));
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
135
+ }, 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, captureNotice: _jsx(CaptureFields, { trace: trace, path: ["result", "answers", key, "probabilities"] }) }, 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
136
  ? "Jev request diagram"
110
137
  : "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: {
111
138
  animationDelay: "delay" in path ? path.delay : undefined,
112
139
  } }), flow &&
113
140
  !showProbabilities &&
114
141
  path.active &&
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: {
142
+ !["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" }), captureFieldUnavailable(trace, ["request", "state"]) ? (_jsx(CaptureFields, { trace: trace, path: ["request", "state"] })) : 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(CaptureFields, { trace: trace, path: ["request", "questions", key] }), _jsx(DiagramContent, { value: question.instructions, label: "Question instructions" }, `${trace.id}:${key}`)] }), _jsxs("article", { className: "lab-card lab-answer-card", style: {
116
143
  left: 390,
117
144
  top: ry,
118
145
  width: 320,
119
146
  minHeight: resultHeight,
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: () => {
147
+ }, 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(CaptureFields, { trace: trace, path: ["result", "answers", key] }), _jsx(PartialBox, { trace: trace, name: key }), !captureFieldUnavailable(trace, ["result", "answers", key]) && (_jsx(Result, { answer: answer, image: image, probabilities: compact ? shownProbabilities : undefined, scaleAvailable: !probabilitiesUnavailable }))] }), 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: () => {
121
148
  if (entering?.question === key && i === entering.to - 1)
122
149
  setEntering(null);
123
150
  }, style: {
@@ -0,0 +1,17 @@
1
+ import type { Json, QuestionView, Trace } from "./protocol.js";
2
+ type CapturedTrace = Omit<Trace, "request" | "result"> & {
3
+ request: Json;
4
+ result?: Json;
5
+ };
6
+ /** Project captured JSON, never the original SDK data. Also accepts legacy
7
+ * captures whose content did not satisfy the old asserted declarations.
8
+ * The transport parser remains responsible for the structural Trace envelope. */
9
+ export declare function projectTraceCapture(trace: CapturedTrace): Trace;
10
+ type Path = readonly string[];
11
+ /** Read only the captured copy, including legacy and whole-value replacements. */
12
+ export declare function capturedValue(trace: Trace, path: Path): Json | undefined;
13
+ export declare function captureQuestionEntries(trace: Trace): [string, QuestionView][];
14
+ export declare function captureFieldUnavailable(trace: Trace, path: Path): boolean;
15
+ export declare function unavailableCaptureFields(trace: Trace, prefix: Path): Path[];
16
+ export declare function traceCaptureView(trace: Trace): Trace;
17
+ export {};
@@ -0,0 +1,266 @@
1
+ const isJsonObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
2
+ const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
3
+ function isQuestionView(value) {
4
+ return isJsonObject(value) && typeof value.type === "string";
5
+ }
6
+ function isAnswerView(value) {
7
+ if (!isJsonObject(value) || typeof value.type !== "string")
8
+ return false;
9
+ if (value.choice !== undefined && typeof value.choice !== "string")
10
+ return false;
11
+ for (const key of ["score", "noul", "confidence"])
12
+ if (value[key] !== undefined && !isFiniteNumber(value[key]))
13
+ return false;
14
+ if (value.probabilities !== undefined &&
15
+ (!isJsonObject(value.probabilities) ||
16
+ !Object.values(value.probabilities).every(isFiniteNumber)))
17
+ return false;
18
+ if (value.legend !== undefined && !isJsonObject(value.legend))
19
+ return false;
20
+ const box = value.box;
21
+ if (box !== undefined &&
22
+ (!isJsonObject(box) ||
23
+ !["left", "top", "right", "bottom"].every((key) => isFiniteNumber(box[key])) ||
24
+ !Object.values(box).every(isFiniteNumber)))
25
+ return false;
26
+ return true;
27
+ }
28
+ function isRequestView(value) {
29
+ return (isJsonObject(value) &&
30
+ typeof value.model === "string" &&
31
+ value.state !== undefined &&
32
+ isJsonObject(value.questions) &&
33
+ Object.values(value.questions).every(isQuestionView));
34
+ }
35
+ function isResultView(value) {
36
+ return (isJsonObject(value) &&
37
+ typeof value.model === "string" &&
38
+ isJsonObject(value.answers) &&
39
+ Object.values(value.answers).every(isAnswerView) &&
40
+ (value.usage === undefined ||
41
+ (isJsonObject(value.usage) &&
42
+ isFiniteNumber(value.usage.input_tokens) &&
43
+ isFiniteNumber(value.usage.output_tokens))));
44
+ }
45
+ function hasStructuredCapture(trace) {
46
+ return (isRequestView(trace.request) &&
47
+ (trace.result === undefined || isResultView(trace.result)));
48
+ }
49
+ /** Project captured JSON, never the original SDK data. Also accepts legacy
50
+ * captures whose content did not satisfy the old asserted declarations.
51
+ * The transport parser remains responsible for the structural Trace envelope. */
52
+ export function projectTraceCapture(trace) {
53
+ if (hasStructuredCapture(trace))
54
+ return trace;
55
+ const requestView = isRequestView(trace.request) ? trace.request : undefined;
56
+ const resultView = trace.result !== undefined && isResultView(trace.result)
57
+ ? trace.result
58
+ : undefined;
59
+ const capture = { ...trace.capture };
60
+ const warnings = new Set(trace.captureWarnings);
61
+ if (!requestView) {
62
+ capture.request = trace.request;
63
+ warnings.add("Request structured view unavailable; see redacted capture in Raw.");
64
+ }
65
+ if (trace.result !== undefined && !resultView) {
66
+ capture.result = trace.result;
67
+ warnings.add("Response structured view unavailable; see redacted capture in Raw.");
68
+ }
69
+ return {
70
+ ...trace,
71
+ request: requestView ?? { model: "", state: null, questions: {} },
72
+ result: resultView,
73
+ ...(Object.keys(capture).length ? { capture } : {}),
74
+ captureWarnings: [...warnings],
75
+ };
76
+ }
77
+ const missing = new WeakMap();
78
+ const startsWith = (path, prefix) => prefix.every((part, index) => path[index] === part);
79
+ /** Read only the captured copy, including legacy and whole-value replacements. */
80
+ export function capturedValue(trace, path) {
81
+ const [section, ...parts] = path;
82
+ let value = section === "request"
83
+ ? (trace.capture?.request ?? trace.request)
84
+ : (trace.capture?.result ?? trace.result);
85
+ // Null is an explicit capture, not a missing capture.
86
+ if (section === "request" && trace.capture?.request === null)
87
+ value = null;
88
+ if (section === "result" && trace.capture?.result === null)
89
+ value = null;
90
+ for (const key of parts)
91
+ value = isJsonObject(value) ? value[key] : undefined;
92
+ return value;
93
+ }
94
+ export function captureQuestionEntries(trace) {
95
+ const questions = { ...trace.request.questions };
96
+ if (trace.capture?.request !== undefined)
97
+ for (const name of Object.keys(trace.result?.answers ?? {}))
98
+ if (!Object.hasOwn(questions, name))
99
+ Object.defineProperty(questions, name, {
100
+ value: { type: "Unavailable" },
101
+ enumerable: true,
102
+ });
103
+ return Object.entries(questions);
104
+ }
105
+ export function captureFieldUnavailable(trace, path) {
106
+ return (missing.get(traceCaptureView(trace)) ?? []).some((entry) => startsWith(path, entry));
107
+ }
108
+ export function unavailableCaptureFields(trace, prefix) {
109
+ return (missing.get(traceCaptureView(trace)) ?? []).filter((entry) => startsWith(entry, prefix));
110
+ }
111
+ // The wire view remains conservative for public consumers and replay. The
112
+ // browser can display valid siblings because every omitted field has a path.
113
+ function partialCaptureView(trace) {
114
+ const paths = [];
115
+ const absent = (path) => {
116
+ paths.push(path);
117
+ };
118
+ const object = (value, path) => {
119
+ if (isJsonObject(value))
120
+ return value;
121
+ absent(path);
122
+ return {};
123
+ };
124
+ const text = (value, path, fallback = "") => {
125
+ if (typeof value === "string")
126
+ return value;
127
+ absent(path);
128
+ return fallback;
129
+ };
130
+ const request = capturedValue(trace, ["request"]);
131
+ const result = capturedValue(trace, ["result"]);
132
+ const r = object(request, ["request"]);
133
+ const questions = object(r.questions, ["request", "questions"]);
134
+ const requestView = {
135
+ ...r,
136
+ model: text(r.model, ["request", "model"]),
137
+ state: r.state ?? null,
138
+ questions: Object.fromEntries(Object.entries(questions).map(([key, value]) => {
139
+ const path = ["request", "questions", key];
140
+ const q = object(value, path);
141
+ return [
142
+ key,
143
+ { ...q, type: text(q.type, [...path, "type"], "Unavailable") },
144
+ ];
145
+ })),
146
+ };
147
+ if (r.state === undefined)
148
+ absent(["request", "state"]);
149
+ let resultView;
150
+ if (result !== undefined) {
151
+ const r = object(result, ["result"]);
152
+ const answers = object(r.answers, ["result", "answers"]);
153
+ resultView = {
154
+ model: text(r.model, ["result", "model"]),
155
+ answers: Object.fromEntries(Object.entries(answers).map(([key, value]) => {
156
+ const path = ["result", "answers", key];
157
+ const a = object(value, path);
158
+ const answer = {
159
+ type: text(a.type, [...path, "type"], "Unavailable"),
160
+ };
161
+ if (a.choice !== undefined) {
162
+ if (typeof a.choice === "string")
163
+ answer.choice = a.choice;
164
+ else
165
+ absent([...path, "choice"]);
166
+ }
167
+ for (const field of ["score", "noul", "confidence"]) {
168
+ if (a[field] === undefined)
169
+ continue;
170
+ if (isFiniteNumber(a[field]))
171
+ answer[field] = a[field];
172
+ else
173
+ absent([...path, field]);
174
+ }
175
+ if (a.probabilities !== undefined) {
176
+ const probabilities = object(a.probabilities, [
177
+ ...path,
178
+ "probabilities",
179
+ ]);
180
+ answer.probabilities = Object.fromEntries(Object.entries(probabilities).flatMap(([label, value]) => {
181
+ if (isFiniteNumber(value))
182
+ return [[label, value]];
183
+ absent([...path, "probabilities", label]);
184
+ return [];
185
+ }));
186
+ }
187
+ if (a.legend !== undefined) {
188
+ if (isJsonObject(a.legend))
189
+ answer.legend = a.legend;
190
+ else
191
+ absent([...path, "legend"]);
192
+ }
193
+ if (a.box !== undefined) {
194
+ const box = object(a.box, [...path, "box"]);
195
+ for (const field of new Set([
196
+ "left",
197
+ "top",
198
+ "right",
199
+ "bottom",
200
+ ...Object.keys(box),
201
+ ])) {
202
+ if (!isFiniteNumber(box[field]))
203
+ absent([...path, "box", field]);
204
+ }
205
+ if (isFiniteNumber(box.left) &&
206
+ isFiniteNumber(box.top) &&
207
+ isFiniteNumber(box.right) &&
208
+ isFiniteNumber(box.bottom)) {
209
+ answer.box = {
210
+ left: box.left,
211
+ top: box.top,
212
+ right: box.right,
213
+ bottom: box.bottom,
214
+ };
215
+ }
216
+ }
217
+ return [key, answer];
218
+ })),
219
+ };
220
+ if (r.usage !== undefined) {
221
+ const usage = object(r.usage, ["result", "usage"]);
222
+ for (const field of ["input_tokens", "output_tokens"])
223
+ if (!isFiniteNumber(usage[field]))
224
+ absent(["result", "usage", field]);
225
+ if (isFiniteNumber(usage.input_tokens) &&
226
+ isFiniteNumber(usage.output_tokens))
227
+ resultView.usage = {
228
+ input_tokens: usage.input_tokens,
229
+ output_tokens: usage.output_tokens,
230
+ };
231
+ }
232
+ }
233
+ if (trace.capture?.request !== undefined)
234
+ for (const key of Object.keys(resultView?.answers ?? {}))
235
+ if (!Object.hasOwn(questions, key))
236
+ absent(["request", "questions", key]);
237
+ // Collapse descendant failures when their entire parent is unavailable.
238
+ const pathKeys = new Set(paths.map((path) => JSON.stringify(path)));
239
+ const unavailable = paths.filter((path) => !path.some((_, index) => pathKeys.has(JSON.stringify(path.slice(0, index)))));
240
+ const view = {
241
+ ...trace,
242
+ request: trace.capture?.request === undefined ? trace.request : requestView,
243
+ result: trace.capture?.result === undefined ? trace.result : resultView,
244
+ captureWarnings: trace.captureWarnings.map((warning) => warning ===
245
+ "Request structured view unavailable; see redacted capture in Raw." ||
246
+ warning ===
247
+ "Response structured view unavailable; see redacted capture in Raw."
248
+ ? "Some captured fields are unavailable. See Raw for captured content."
249
+ : warning),
250
+ };
251
+ view.captureWarnings = [...new Set(view.captureWarnings)];
252
+ missing.set(view, unavailable);
253
+ return view;
254
+ }
255
+ // Browser snapshots are immutable. Share the projection across consumers.
256
+ const views = new WeakMap();
257
+ export function traceCaptureView(trace) {
258
+ const cached = views.get(trace);
259
+ if (cached)
260
+ return cached;
261
+ const structured = projectTraceCapture(trace);
262
+ const view = structured.capture ? partialCaptureView(structured) : structured;
263
+ views.set(trace, view);
264
+ views.set(view, view);
265
+ return view;
266
+ }
@@ -15,8 +15,12 @@ export function conditionInputs(rule, run, traces, stepNames = new Map()) {
15
15
  const trace = typeof node.stepId === "string"
16
16
  ? traces.get(run.steps[node.stepId]?.traceId ?? "")
17
17
  : undefined;
18
- let value = trace?.result;
19
- let available = !!trace?.result;
18
+ // Incoming labels display captured JSON; they never evaluate a rule or
19
+ // require the numeric answer shape used by the request diagrams.
20
+ let value = trace?.capture?.result !== undefined
21
+ ? trace.capture.result
22
+ : trace?.result;
23
+ let available = value !== undefined;
20
24
  if (node.source === "task" && typeof node.stepId === "string") {
21
25
  const data = run.steps[node.stepId]?.data;
22
26
  available =
@@ -87,7 +91,8 @@ export function conditionRequirement(rule, inputs) {
87
91
  !Array.isArray(right) &&
88
92
  left.op === "reference" &&
89
93
  right.op === "literal" &&
90
- ["gte", "lt", "gt", "eq"].includes(String(rule.op))) {
94
+ typeof rule.op === "string" &&
95
+ ["gte", "lt", "gt", "eq"].includes(rule.op)) {
91
96
  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
97
  }
93
98
  }
@@ -98,7 +103,7 @@ export function conditionRequirement(rule, inputs) {
98
103
  return {
99
104
  ...node,
100
105
  label: typeof node.label === "string"
101
- ? names.get(node.label) ?? "Input"
106
+ ? (names.get(node.label) ?? "Input")
102
107
  : "Input",
103
108
  };
104
109
  if (Array.isArray(node.args))
@@ -1,3 +1,4 @@
1
+ import { traceCaptureView } from "./capture-view.js";
1
2
  // Presentation only. Credential and application redaction happen in the server
2
3
  // recorder; these personal fields are still present in the received captures.
3
4
  const sensitiveKeys = new Set([
@@ -64,16 +65,22 @@ export function createDisplayRedactor() {
64
65
  // Mask their contents, not the entry itself because its name resembles PII.
65
66
  const question = (value) => fields(value, { criteria: contents });
66
67
  const answer = (value) => fields(value, { probabilities: contents, legend: contents });
68
+ const request = (value) => fields(value, {
69
+ questions: (items) => mapValues(items, question),
70
+ assets: contents,
71
+ });
72
+ const result = (value) => fields(value, { answers: (items) => mapValues(items, answer) });
67
73
  const trace = memoize((value) => {
68
- const request = fields(value.request, {
69
- questions: (items) => mapValues(items, question),
70
- assets: contents,
71
- }), result = fields(value.result, {
72
- answers: (items) => mapValues(items, answer),
73
- });
74
- return request === value.request && result === value.result
74
+ value = traceCaptureView(value);
75
+ // Fallback JSON keeps the same SDK map roles as structured views, even
76
+ // when a sibling field has been replaced. Personal content still masks.
77
+ const capture = value.capture && fields(value.capture, { request, result });
78
+ const maskedRequest = request(value.request), maskedResult = result(value.result);
79
+ return maskedRequest === value.request &&
80
+ maskedResult === value.result &&
81
+ capture === value.capture
75
82
  ? value
76
- : { ...value, request, result };
83
+ : { ...value, request: maskedRequest, result: maskedResult, capture };
77
84
  });
78
85
  const run = memoize((value) => {
79
86
  let changed = false;
@@ -30,7 +30,8 @@ export function formatFlowCondition(value, depth = 0) {
30
30
  : "FALSE (no conditions)";
31
31
  return `(${args.map(format).join(value.op === "all" ? " AND " : " OR ")})`;
32
32
  }
33
- if (!["eq", "lt", "gt", "gte"].includes(String(value.op)) ||
33
+ if (typeof value.op !== "string" ||
34
+ !["eq", "lt", "gt", "gte"].includes(value.op) ||
34
35
  args.length !== 2)
35
36
  return "[rule unavailable]";
36
37
  const probability = args.some((arg) => arg &&
@@ -1,3 +1,4 @@
1
+ import { projectTraceCapture } from "./capture-view.js";
1
2
  /** Builds a replay fixture from the captured answers of a run's completed jev steps, including map and loop items. */
2
3
  export function flowFixture(definition, run, traces) {
3
4
  const steps = {}, missing = [], warnings = new Set();
@@ -11,14 +12,15 @@ export function flowFixture(definition, run, traces) {
11
12
  collect(node.itemDefinition, item.run, `${prefix}${node.id}/${item.index}/`);
12
13
  if (node.kind !== "jev" || step?.status !== "completed")
13
14
  continue;
14
- const trace = step.traceId ? traces.get(step.traceId) : undefined;
15
- if (!trace?.result) {
15
+ const captured = step.traceId ? traces.get(step.traceId) : undefined;
16
+ const trace = captured && projectTraceCapture(captured);
17
+ for (const warning of trace?.captureWarnings ?? [])
18
+ warnings.add(warning);
19
+ if (!trace?.result || trace.capture?.result !== undefined) {
16
20
  missing.push(prefix + node.id);
17
21
  continue;
18
22
  }
19
23
  steps[prefix + node.id] = structuredClone(trace.result);
20
- for (const warning of trace.captureWarnings)
21
- warnings.add(warning);
22
24
  }
23
25
  };
24
26
  collect(definition, run, "");
@@ -21,10 +21,10 @@ export function flowAnswer(answer) {
21
21
  active: label === answer.choice,
22
22
  eyebrow: label === answer.choice ? "RETURNED CHOICE" : "ALTERNATIVE",
23
23
  }));
24
- if (answer.type === "score")
24
+ if (answer.type === "score" && answer.score !== undefined)
25
25
  return [
26
26
  {
27
- label: `Score ${answer.score ?? "unavailable"}`,
27
+ label: `Score ${answer.score}`,
28
28
  detail: "Returned by Jev",
29
29
  active: true,
30
30
  eyebrow: "RETURNED SCORE",
@@ -47,7 +47,7 @@ export function flowAnswer(answer) {
47
47
  probability: 1 - answer.noul,
48
48
  },
49
49
  ];
50
- if (answer.type === "box")
50
+ if (answer.type === "box" && answer.box)
51
51
  return [
52
52
  {
53
53
  label: "Region coordinates",
@@ -0,0 +1,6 @@
1
+ import type { FlowDefinition, FlowRun } from "./flow-protocol.js";
2
+ /** Validate an entire event in staging maps before publishing either cache. */
3
+ export declare function parseFlowUpdate(input: unknown, currentRuns: ReadonlyMap<string, FlowRun>, currentDefinitions: ReadonlyMap<string, FlowDefinition>): {
4
+ runs: Map<string, FlowRun>;
5
+ definitions: Map<string, FlowDefinition>;
6
+ };