jev-devtools 0.5.3 → 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.
- package/README.md +109 -103
- package/lib/flow/index.d.ts +6 -3
- package/lib/flow/index.js +3 -2
- package/lib/server/recorder.js +11 -9
- package/lib/src/CaptureFields.d.ts +11 -0
- package/lib/src/CaptureFields.js +42 -0
- package/lib/src/ChoiceProbabilities.d.ts +3 -1
- package/lib/src/ChoiceProbabilities.js +2 -2
- package/lib/src/JevDevtools.d.ts +1 -1
- package/lib/src/JevDevtools.js +21 -6
- package/lib/src/Questions.js +10 -5
- package/lib/src/TraceDiagram.js +45 -18
- package/lib/src/capture-view.d.ts +17 -0
- package/lib/src/capture-view.js +266 -0
- package/lib/src/condition-inputs.js +9 -4
- package/lib/src/display-redaction.js +15 -8
- package/lib/src/flow-condition.js +2 -1
- package/lib/src/flow-fixture.js +6 -4
- package/lib/src/flow-model.js +3 -3
- package/lib/src/flow-parser.d.ts +6 -0
- package/lib/src/flow-parser.js +191 -0
- package/lib/src/flow-stream.js +12 -31
- package/lib/src/inspector-styles.js +1 -1
- package/lib/src/inspector.css +1 -0
- package/lib/src/protocol.d.ts +7 -0
- package/lib/src/trace-parser.d.ts +7 -0
- package/lib/src/trace-parser.js +27 -0
- package/lib/src/trace-store.js +16 -24
- package/lib/src/transport-validation.d.ts +20 -0
- package/lib/src/transport-validation.js +45 -0
- package/package.json +1 -1
- package/skills/setup-jev-devtools/SKILL.md +2 -2
- package/skills/setup-jev-devtools/references/flows.md +38 -2
- package/skills/setup-jev-devtools/references/verify.md +42 -33
package/lib/src/TraceDiagram.js
CHANGED
|
@@ -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 =
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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
|
-
{
|
|
88
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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 (
|
|
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 &&
|
package/lib/src/flow-fixture.js
CHANGED
|
@@ -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
|
|
15
|
-
|
|
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, "");
|
package/lib/src/flow-model.js
CHANGED
|
@@ -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
|
|
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
|
+
};
|