jev-devtools 0.5.1 → 0.5.2
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 +49 -27
- package/lib/flow/index.d.ts +32 -4
- package/lib/flow/index.js +133 -55
- package/lib/server/flows.js +14 -3
- package/lib/server/recorder.js +9 -3
- package/lib/src/FlowOverview.js +50 -59
- package/lib/src/JevDevtools.js +31 -5
- package/lib/src/TraceDiagram.js +1 -1
- package/lib/src/flow-protocol.d.ts +4 -0
- package/lib/src/inspector-styles.js +1 -1
- package/lib/src/inspector.css +154 -17
- package/lib/src/useDiagramViewport.js +1 -1
- package/package.json +1 -1
- package/skills/setup-jev-devtools/SKILL.md +9 -9
- package/skills/setup-jev-devtools/references/flows.md +188 -111
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Jev DevTools
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/jev-devtools/v/0.5.2)
|
|
4
4
|
|
|
5
5
|
A React drawer and Node adapter for inspecting TypeSafe Jev requests with animated diagrams. Includes request history, context, questions, typed answers, transport attempts, and raw snapshots.
|
|
6
6
|
|
|
7
|
-
**Source version: 0.5.
|
|
7
|
+
**Source version: 0.5.2 — pending publication.** npm currently provides 0.5.1. Check the [release history](https://github.com/diegoag64/jev-devtools/blob/main/CHANGELOG.md) for compatibility notes. To try this release candidate, install the local archive below. **Experimental (pre-1.0).** React 18/19 and Node 22.12+ are supported. APIs may change before 1.0. Verification uses sample responses through the real TypeSafe SDK, not live inference.
|
|
8
8
|
|
|
9
9
|
[Website](https://jevtools.dev/) · [Documentation](https://docs.jevtools.dev/) · [Quick start](https://docs.jevtools.dev/getting-started/) · [API reference](https://docs.jevtools.dev/api/reference/) · [Release history](https://docs.jevtools.dev/changelog/)
|
|
10
10
|
|
|
@@ -48,25 +48,25 @@ To try local changes before a future release:
|
|
|
48
48
|
npm ci
|
|
49
49
|
npm run pack:devtools
|
|
50
50
|
# From the consuming app:
|
|
51
|
-
npm install /absolute/path/to/jev-devtools-0.5.
|
|
51
|
+
npm install /absolute/path/to/jev-devtools-0.5.2.tgz
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
Mount once. The drawer loads its own styles; no CSS import is needed:
|
|
55
55
|
|
|
56
56
|
```tsx
|
|
57
|
-
import { JevDevtools } from
|
|
57
|
+
import { JevDevtools } from "jev-devtools";
|
|
58
58
|
|
|
59
|
-
<JevDevtools enabled={import.meta.env.DEV} defaultOpen={false}
|
|
59
|
+
<JevDevtools enabled={import.meta.env.DEV} defaultOpen={false} />;
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
Create one shared Node adapter and instrumented client:
|
|
63
63
|
|
|
64
64
|
```ts
|
|
65
|
-
import { createJevDevtools } from
|
|
65
|
+
import { createJevDevtools } from "jev-devtools/server";
|
|
66
66
|
|
|
67
67
|
const inspector = createJevDevtools({
|
|
68
|
-
enabled: process.env.NODE_ENV ===
|
|
69
|
-
origin:
|
|
68
|
+
enabled: process.env.NODE_ENV === "development",
|
|
69
|
+
origin: "http://localhost:3000",
|
|
70
70
|
});
|
|
71
71
|
const jev = inspector.createClient({ apiKey: process.env.TYPESAFE_API_KEY });
|
|
72
72
|
app.use(inspector.middleware); // Mount at root, before fallback handlers.
|
|
@@ -95,27 +95,49 @@ Define calls, conditions, parallel checks, and named outcomes with `jev-devtools
|
|
|
95
95
|
On the server, reuse the instrumented `jev` client and `inspector` from the setup above:
|
|
96
96
|
|
|
97
97
|
```ts
|
|
98
|
-
import { defineJevFlow } from
|
|
99
|
-
|
|
100
|
-
const review = defineJevFlow<{ message: string }>(
|
|
101
|
-
const decision = flow.jev(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
98
|
+
import { defineJevFlow } from "jev-devtools/flow";
|
|
99
|
+
|
|
100
|
+
const review = defineJevFlow<{ message: string }>("review", (flow) => {
|
|
101
|
+
const decision = flow.jev(
|
|
102
|
+
"readiness",
|
|
103
|
+
{
|
|
104
|
+
state: flow.input,
|
|
105
|
+
questions: {
|
|
106
|
+
ready: { type: "noul", instructions: "Is this ready for review?" },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{ title: "Assess readiness" },
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return flow.branch(
|
|
113
|
+
"route",
|
|
114
|
+
{
|
|
115
|
+
when: decision.answer("ready").noul.gte(0.9), // Your application's policy.
|
|
116
|
+
then: (path) =>
|
|
117
|
+
path.output(
|
|
118
|
+
"ready",
|
|
119
|
+
{ probability: decision.answer("ready").noul },
|
|
120
|
+
{
|
|
121
|
+
title: "Ready for review",
|
|
122
|
+
description: "Return readiness to the application.",
|
|
123
|
+
},
|
|
124
|
+
),
|
|
125
|
+
else: (path) =>
|
|
126
|
+
path.output("manual-review", undefined, {
|
|
127
|
+
title: "Needs manual review",
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
{ title: "Require at least 90% readiness" },
|
|
131
|
+
);
|
|
113
132
|
});
|
|
114
133
|
|
|
115
|
-
const result = await review.run(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
134
|
+
const result = await review.run(
|
|
135
|
+
{ message: "Please review my ticket." },
|
|
136
|
+
{
|
|
137
|
+
client: jev,
|
|
138
|
+
devtools: inspector,
|
|
139
|
+
},
|
|
140
|
+
);
|
|
119
141
|
// Handle result.outcome in your app. The flow does not execute business actions.
|
|
120
142
|
```
|
|
121
143
|
|
package/lib/flow/index.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ type Request<Q extends Questions> = Omit<SystemOneRequest<Q>, "state"> & {
|
|
|
53
53
|
state: unknown;
|
|
54
54
|
};
|
|
55
55
|
/** Resolve nested references before invoking a server callback. */
|
|
56
|
-
export type Resolved<T> = T extends FlowExpression<infer V> ? V : T extends readonly unknown[] ? {
|
|
56
|
+
export type Resolved<T> = T extends FlowExpression<infer V> ? V : T extends string | number | boolean | null | undefined ? T : T extends readonly unknown[] ? {
|
|
57
57
|
[K in keyof T]: Resolved<T[K]>;
|
|
58
58
|
} : T extends object ? {
|
|
59
59
|
[K in keyof T]: Resolved<T[K]>;
|
|
@@ -100,9 +100,11 @@ export interface FlowBuilder<T> {
|
|
|
100
100
|
jev<const Q extends Questions>(id: string, request: Request<Q>, display?: FlowDisplay): AnswerRefs<Q>;
|
|
101
101
|
jev<I, const Q extends Questions>(id: string, request: FlowRequestFactory<I, Q>, display?: FlowDisplay): AnswerRefs<Q>;
|
|
102
102
|
task<I, O>(id: string, task: FlowTask<I, O>, display?: FlowDisplay): FlowExpression<Awaited<O>>;
|
|
103
|
-
/**
|
|
103
|
+
/** References and scalar arrays already carry their resolved item type. */
|
|
104
104
|
map<I, C, O>(id: string, options: {
|
|
105
|
-
items: FlowExpression<readonly I[]> | readonly I[]
|
|
105
|
+
items: FlowExpression<readonly I[]> | (readonly I[] & readonly (string | number | {
|
|
106
|
+
valueOf(): boolean;
|
|
107
|
+
} | null)[]);
|
|
106
108
|
input: C;
|
|
107
109
|
concurrency?: number;
|
|
108
110
|
maxItems: number;
|
|
@@ -111,17 +113,43 @@ export interface FlowBuilder<T> {
|
|
|
111
113
|
index: number;
|
|
112
114
|
context: Resolved<C>;
|
|
113
115
|
}>) => FlowExpression<O>, display?: FlowDisplay): FlowExpression<O[]>;
|
|
114
|
-
/**
|
|
116
|
+
/** Execute a reusable program over resolved items; return results in input order. */
|
|
117
|
+
map<I, C, O>(id: string, options: {
|
|
118
|
+
items: FlowExpression<readonly I[]> | readonly I[];
|
|
119
|
+
input: C;
|
|
120
|
+
concurrency?: number;
|
|
121
|
+
maxItems: number;
|
|
122
|
+
}, build: (item: FlowBuilder<{
|
|
123
|
+
item: Resolved<I>;
|
|
124
|
+
index: number;
|
|
125
|
+
context: Resolved<C>;
|
|
126
|
+
}>) => FlowExpression<O>, display?: FlowDisplay): FlowExpression<O[]>;
|
|
127
|
+
/** References and scalar initial values preserve their generic state type. */
|
|
115
128
|
loop<S, C>(id: string, options: {
|
|
116
129
|
initial: S | FlowExpression<S>;
|
|
117
130
|
input: C;
|
|
118
131
|
while: (state: FlowExpression<NoInfer<S>>) => FlowExpression<boolean>;
|
|
119
132
|
maxIterations: number;
|
|
133
|
+
} & {
|
|
134
|
+
initial: FlowExpression<S> | string | number | {
|
|
135
|
+
valueOf(): boolean;
|
|
136
|
+
} | null;
|
|
120
137
|
}, build: (iteration: FlowBuilder<{
|
|
121
138
|
state: S;
|
|
122
139
|
index: number;
|
|
123
140
|
context: Resolved<C>;
|
|
124
141
|
}>) => FlowExpression<NoInfer<S>>, display?: FlowDisplay): FlowExpression<S>;
|
|
142
|
+
/** Pre-test loop over resolved JSON state, with explicit context and an iteration limit. */
|
|
143
|
+
loop<S, C>(id: string, options: {
|
|
144
|
+
initial: S | FlowExpression<S>;
|
|
145
|
+
input: C;
|
|
146
|
+
while: (state: FlowExpression<NoInfer<Resolved<S>>>) => FlowExpression<boolean>;
|
|
147
|
+
maxIterations: number;
|
|
148
|
+
}, build: (iteration: FlowBuilder<{
|
|
149
|
+
state: Resolved<S>;
|
|
150
|
+
index: number;
|
|
151
|
+
context: Resolved<C>;
|
|
152
|
+
}>) => FlowExpression<NoInfer<Resolved<S>>>, display?: FlowDisplay): FlowExpression<Resolved<S>>;
|
|
125
153
|
/** Execute one value-returning path, then continue with its typed result. */
|
|
126
154
|
if<A, B>(id: string, paths: {
|
|
127
155
|
when: FlowExpression<boolean>;
|
package/lib/flow/index.js
CHANGED
|
@@ -6,21 +6,38 @@ export { flowFixture } from "../src/flow-fixture.js";
|
|
|
6
6
|
export { compareWithFixture, formatComparison, } from "./compare.js";
|
|
7
7
|
const owners = new WeakMap();
|
|
8
8
|
const builderIds = new WeakMap();
|
|
9
|
+
const astBudgets = new WeakMap();
|
|
9
10
|
function freezeAst(ast, depth = 0) {
|
|
10
11
|
if (depth > 80)
|
|
11
12
|
throw new Error("Expression nesting exceeds 80");
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
const cached = astBudgets.get(ast);
|
|
14
|
+
if (cached) {
|
|
15
|
+
if (depth + cached.depth > 80)
|
|
16
|
+
throw new Error("Expression nesting exceeds 80");
|
|
17
|
+
return cached;
|
|
18
|
+
}
|
|
19
|
+
const budget = { depth: 0, nodes: 1 };
|
|
20
|
+
const include = (child) => {
|
|
21
|
+
const childBudget = freezeAst(child, depth + 1);
|
|
22
|
+
budget.depth = Math.max(budget.depth, childBudget.depth + 1);
|
|
23
|
+
// Visitors expand each occurrence, even when multiple operands share a child.
|
|
24
|
+
budget.nodes += childBudget.nodes;
|
|
25
|
+
if (budget.nodes > 4096)
|
|
26
|
+
throw new Error("Expression size exceeds 4096 expanded nodes");
|
|
27
|
+
};
|
|
14
28
|
if (ast.op === "get")
|
|
15
|
-
|
|
29
|
+
include(ast.from);
|
|
16
30
|
if ("args" in ast) {
|
|
17
31
|
if (ast.args.length > 256)
|
|
18
32
|
throw new Error("Too many condition operands");
|
|
19
33
|
for (const child of ast.args)
|
|
20
|
-
|
|
34
|
+
include(child);
|
|
21
35
|
Object.freeze(ast.args);
|
|
22
36
|
}
|
|
23
37
|
Object.freeze(ast);
|
|
38
|
+
// Frozen caller-supplied ASTs still need validation; only our cache is trusted.
|
|
39
|
+
astBudgets.set(ast, budget);
|
|
40
|
+
return budget;
|
|
24
41
|
}
|
|
25
42
|
/** Inspectable expression, not an application callback. Use get() for input fields. */
|
|
26
43
|
export class FlowExpression {
|
|
@@ -123,21 +140,47 @@ const inspectAst = (ast, ref) => {
|
|
|
123
140
|
ast.args.forEach((a) => inspectAst(a, ref));
|
|
124
141
|
};
|
|
125
142
|
function describe(ast) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return
|
|
134
|
-
|
|
143
|
+
const limit = 4096;
|
|
144
|
+
const chunks = [];
|
|
145
|
+
let remaining = limit;
|
|
146
|
+
const append = (text) => {
|
|
147
|
+
chunks.push(text.slice(0, remaining));
|
|
148
|
+
const fits = text.length <= remaining;
|
|
149
|
+
remaining -= Math.min(text.length, remaining);
|
|
150
|
+
return fits;
|
|
151
|
+
};
|
|
152
|
+
const visit = (node) => {
|
|
153
|
+
if (node.op === "input")
|
|
154
|
+
return append("input");
|
|
155
|
+
if (node.op === "ref")
|
|
156
|
+
return append(node.id);
|
|
157
|
+
if (node.op === "get")
|
|
158
|
+
return visit(node.from) && append(".") && append(node.key);
|
|
159
|
+
if (node.op === "literal")
|
|
160
|
+
return append("[literal]");
|
|
161
|
+
if (!append(node.op) || !append("("))
|
|
162
|
+
return false;
|
|
163
|
+
for (let i = 0; i < node.args.length; i++) {
|
|
164
|
+
if (i > 0 && !append(", "))
|
|
165
|
+
return false;
|
|
166
|
+
if (!visit(node.args[i]))
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
return append(")");
|
|
170
|
+
};
|
|
171
|
+
const complete = visit(ast);
|
|
172
|
+
const text = chunks.join("");
|
|
173
|
+
const marker = "[condition truncated]";
|
|
174
|
+
return complete ? text : text.slice(0, limit - marker.length) + marker;
|
|
135
175
|
}
|
|
136
176
|
function conditionRule(ast, requests, tasks) {
|
|
137
177
|
let remaining = 128;
|
|
178
|
+
const warnings = new Set();
|
|
138
179
|
const visit = (node) => {
|
|
139
|
-
if (--remaining < 0)
|
|
180
|
+
if (--remaining < 0) {
|
|
181
|
+
warnings.add("Rule projection limit reached.");
|
|
140
182
|
return { op: "literal", value: "[rule truncated]" };
|
|
183
|
+
}
|
|
141
184
|
if (node.op === "input" || node.op === "ref" || node.op === "get") {
|
|
142
185
|
const path = [];
|
|
143
186
|
let base = node;
|
|
@@ -179,16 +222,20 @@ function conditionRule(ast, requests, tasks) {
|
|
|
179
222
|
: {}),
|
|
180
223
|
};
|
|
181
224
|
}
|
|
182
|
-
if (node.op === "literal")
|
|
225
|
+
if (node.op === "literal") {
|
|
226
|
+
const scalar = node.value === null ||
|
|
227
|
+
typeof node.value === "string" ||
|
|
228
|
+
typeof node.value === "boolean" ||
|
|
229
|
+
(typeof node.value === "number" && Number.isFinite(node.value));
|
|
230
|
+
if (!scalar)
|
|
231
|
+
warnings.add("Rule literal capture unavailable.");
|
|
183
232
|
return {
|
|
184
233
|
op: "literal",
|
|
185
|
-
value:
|
|
186
|
-
typeof node.value === "string" ||
|
|
187
|
-
typeof node.value === "boolean" ||
|
|
188
|
-
(typeof node.value === "number" && Number.isFinite(node.value))
|
|
234
|
+
value: scalar
|
|
189
235
|
? node.value
|
|
190
236
|
: "[value unavailable]",
|
|
191
237
|
};
|
|
238
|
+
}
|
|
192
239
|
const args = [];
|
|
193
240
|
for (const arg of node.args) {
|
|
194
241
|
args.push(visit(arg));
|
|
@@ -197,7 +244,8 @@ function conditionRule(ast, requests, tasks) {
|
|
|
197
244
|
}
|
|
198
245
|
return { op: node.op, args };
|
|
199
246
|
};
|
|
200
|
-
|
|
247
|
+
const rule = visit(ast);
|
|
248
|
+
return { conditionRule: rule, conditionCaptureWarnings: [...warnings] };
|
|
201
249
|
}
|
|
202
250
|
function evaluate(ast, input, values) {
|
|
203
251
|
if (ast.op === "input")
|
|
@@ -492,7 +540,7 @@ export function defineJevFlow(name, build) {
|
|
|
492
540
|
return { ...ast, args: ast.args.map(referenceState) };
|
|
493
541
|
return ast;
|
|
494
542
|
};
|
|
495
|
-
node
|
|
543
|
+
Object.assign(node, conditionRule(referenceState(when.ast), new Set(), new Set([id])));
|
|
496
544
|
return new FlowExpression({ op: "ref", id }, owner);
|
|
497
545
|
},
|
|
498
546
|
if(id, paths, display) {
|
|
@@ -502,9 +550,11 @@ export function defineJevFlow(name, build) {
|
|
|
502
550
|
step.when = paths.when;
|
|
503
551
|
const node = nodes.find((n) => n.id === id);
|
|
504
552
|
node.condition = describe(paths.when.ast);
|
|
505
|
-
node
|
|
506
|
-
.filter((n) => n.kind === "task" ||
|
|
507
|
-
.
|
|
553
|
+
Object.assign(node, conditionRule(paths.when.ast, new Set(nodes.filter((n) => n.kind === "jev").map((n) => n.id)), new Set(nodes
|
|
554
|
+
.filter((n) => n.kind === "task" ||
|
|
555
|
+
n.kind === "merge" ||
|
|
556
|
+
n.kind === "loop")
|
|
557
|
+
.map((n) => n.id))));
|
|
508
558
|
step.then = block(paths.then, new Set(available), [...path, `${id}:then`], [id], depth + 1, false, true);
|
|
509
559
|
step.else = block(paths.else, new Set(available), [...path, `${id}:else`], [id], depth + 1, false, true);
|
|
510
560
|
// A separate merge keeps the diagram acyclic and gives downstream refs a
|
|
@@ -534,9 +584,11 @@ export function defineJevFlow(name, build) {
|
|
|
534
584
|
throw new Error("branch requires a condition expression");
|
|
535
585
|
const node = nodes.find((n) => n.id === id);
|
|
536
586
|
node.condition = describe(paths.when.ast);
|
|
537
|
-
node
|
|
538
|
-
.filter((n) => n.kind === "task" ||
|
|
539
|
-
.
|
|
587
|
+
Object.assign(node, conditionRule(paths.when.ast, new Set(nodes.filter((n) => n.kind === "jev").map((n) => n.id)), new Set(nodes
|
|
588
|
+
.filter((n) => n.kind === "task" ||
|
|
589
|
+
n.kind === "merge" ||
|
|
590
|
+
n.kind === "loop")
|
|
591
|
+
.map((n) => n.id))));
|
|
540
592
|
step.then = block(paths.then, new Set(available), [...path, `${id}:then`], [id], depth + 1, true);
|
|
541
593
|
step.else = block(paths.else, new Set(available), [...path, `${id}:else`], [id], depth + 1, true);
|
|
542
594
|
terminal = true;
|
|
@@ -624,6 +676,7 @@ export function defineJevFlow(name, build) {
|
|
|
624
676
|
if (node.lanes)
|
|
625
677
|
Object.freeze(node.lanes);
|
|
626
678
|
freezeRule(node.conditionRule);
|
|
679
|
+
freezeRule(node.conditionCaptureWarnings);
|
|
627
680
|
freezeRule(node.itemDefinition);
|
|
628
681
|
Object.freeze(node);
|
|
629
682
|
}
|
|
@@ -684,49 +737,73 @@ export function defineJevFlow(name, build) {
|
|
|
684
737
|
if (value && typeof value === "object" && observerData.has(value))
|
|
685
738
|
return observerData.get(value);
|
|
686
739
|
let snapshot;
|
|
740
|
+
const warnings = [];
|
|
687
741
|
try {
|
|
688
742
|
snapshot = observerSnapshot(value);
|
|
689
743
|
}
|
|
690
744
|
catch {
|
|
691
745
|
// Direct flow outputs may be non-JSON; only their capture is unavailable.
|
|
692
746
|
snapshot = "[capture unavailable]";
|
|
747
|
+
warnings.push("Observer capture unavailable.");
|
|
693
748
|
}
|
|
749
|
+
const captured = { data: snapshot, warnings };
|
|
694
750
|
if (value && typeof value === "object")
|
|
695
|
-
observerData.set(value,
|
|
696
|
-
return
|
|
751
|
+
observerData.set(value, captured);
|
|
752
|
+
return captured;
|
|
697
753
|
};
|
|
698
754
|
const emit = () => {
|
|
699
755
|
try {
|
|
700
756
|
if (observer) {
|
|
701
757
|
const { value, steps, ...metadata } = run;
|
|
758
|
+
const outcome = value !== undefined ? snapshotData(value) : undefined;
|
|
702
759
|
observer.update({
|
|
703
760
|
...structuredClone(metadata),
|
|
704
|
-
steps: Object.fromEntries(Object.entries(steps).map(([id, step]) =>
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
? {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
761
|
+
steps: Object.fromEntries(Object.entries(steps).map(([id, step]) => {
|
|
762
|
+
const data = step.data !== undefined
|
|
763
|
+
? snapshotData(step.data)
|
|
764
|
+
: undefined;
|
|
765
|
+
return [
|
|
766
|
+
id,
|
|
767
|
+
{
|
|
768
|
+
...step,
|
|
769
|
+
...(data ? { data: data.data } : {}),
|
|
770
|
+
...(step.loop ? { loop: { ...step.loop } } : {}),
|
|
771
|
+
...(data || step.captureWarnings
|
|
772
|
+
? {
|
|
773
|
+
captureWarnings: [
|
|
774
|
+
...new Set([
|
|
775
|
+
...(step.captureWarnings ?? []),
|
|
776
|
+
...(data?.warnings ?? []),
|
|
777
|
+
]),
|
|
778
|
+
],
|
|
779
|
+
}
|
|
780
|
+
: {}),
|
|
781
|
+
...(step.collection
|
|
782
|
+
? {
|
|
783
|
+
collection: {
|
|
784
|
+
...step.collection,
|
|
785
|
+
// Child runs have already crossed this snapshot boundary.
|
|
786
|
+
// Copy mutable collection metadata, reuse detached children.
|
|
787
|
+
items: step.collection.items.map((item) => ({
|
|
788
|
+
...item,
|
|
789
|
+
})),
|
|
790
|
+
},
|
|
791
|
+
}
|
|
792
|
+
: {}),
|
|
793
|
+
},
|
|
794
|
+
];
|
|
795
|
+
})),
|
|
796
|
+
...(outcome
|
|
797
|
+
? {
|
|
798
|
+
value: outcome.data,
|
|
799
|
+
captureWarnings: [
|
|
800
|
+
...new Set([
|
|
801
|
+
...(run.captureWarnings ?? []),
|
|
802
|
+
...outcome.warnings,
|
|
803
|
+
]),
|
|
804
|
+
],
|
|
805
|
+
}
|
|
806
|
+
: {}),
|
|
730
807
|
});
|
|
731
808
|
}
|
|
732
809
|
}
|
|
@@ -1004,6 +1081,7 @@ export function defineJevFlow(name, build) {
|
|
|
1004
1081
|
}
|
|
1005
1082
|
catch {
|
|
1006
1083
|
state.data = "Selected value could not be captured as JSON.";
|
|
1084
|
+
state.captureWarnings = ["Selected value capture unavailable."];
|
|
1007
1085
|
}
|
|
1008
1086
|
}
|
|
1009
1087
|
else if (step.kind === "branch" || step.kind === "if") {
|
package/lib/server/flows.js
CHANGED
|
@@ -36,14 +36,17 @@ export class FlowRecorder {
|
|
|
36
36
|
redactRules(node.itemDefinition, [...prefix, node.id, "items"]);
|
|
37
37
|
if (node.conditionRule === undefined)
|
|
38
38
|
continue;
|
|
39
|
+
const warnings = new Set(node.conditionCaptureWarnings);
|
|
39
40
|
try {
|
|
40
|
-
node.conditionRule = capture(node.conditionRule,
|
|
41
|
+
node.conditionRule = capture(node.conditionRule, warnings, this.options.redact
|
|
41
42
|
? (path, value) => this.options.redact(["conditions", ...prefix, node.id, ...path], value)
|
|
42
43
|
: undefined);
|
|
43
44
|
}
|
|
44
45
|
catch {
|
|
45
46
|
node.conditionRule = "[rule capture unavailable]";
|
|
47
|
+
warnings.add("Rule capture unavailable.");
|
|
46
48
|
}
|
|
49
|
+
node.conditionCaptureWarnings = [...warnings];
|
|
47
50
|
}
|
|
48
51
|
};
|
|
49
52
|
redactRules(stored);
|
|
@@ -117,20 +120,28 @@ export class FlowRecorder {
|
|
|
117
120
|
{
|
|
118
121
|
...step,
|
|
119
122
|
data: captured.data,
|
|
120
|
-
captureWarnings:
|
|
123
|
+
captureWarnings: [
|
|
124
|
+
...new Set([
|
|
125
|
+
...(step.captureWarnings ?? []),
|
|
126
|
+
...captured.warnings,
|
|
127
|
+
]),
|
|
128
|
+
],
|
|
121
129
|
},
|
|
122
130
|
];
|
|
123
131
|
})),
|
|
124
132
|
};
|
|
125
133
|
if ("value" in stored) {
|
|
134
|
+
const warnings = new Set(run.captureWarnings);
|
|
126
135
|
try {
|
|
127
|
-
stored.value = capture(stored.value,
|
|
136
|
+
stored.value = capture(stored.value, warnings, this.options.redact
|
|
128
137
|
? (path, value) => this.options.redact([...prefix, ...path], value)
|
|
129
138
|
: undefined);
|
|
130
139
|
}
|
|
131
140
|
catch {
|
|
132
141
|
stored.value = "[capture unavailable]";
|
|
142
|
+
warnings.add("Outcome capture unavailable.");
|
|
133
143
|
}
|
|
144
|
+
stored.captureWarnings = [...warnings];
|
|
134
145
|
}
|
|
135
146
|
if (prefix.length)
|
|
136
147
|
this.itemCaptures.set(run, stored);
|
package/lib/server/recorder.js
CHANGED
|
@@ -36,10 +36,16 @@ export function capture(value, warnings, redact) {
|
|
|
36
36
|
let budget = 96_000;
|
|
37
37
|
const visit = (input, path, depth) => {
|
|
38
38
|
const key = (path.at(-1) ?? "").replace(/[-_]/g, "").toLowerCase();
|
|
39
|
-
if (credentialKeys.has(key))
|
|
39
|
+
if (credentialKeys.has(key)) {
|
|
40
|
+
warnings.add("Capture contains redacted values.");
|
|
40
41
|
return "[redacted]";
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
}
|
|
43
|
+
if (redact) {
|
|
44
|
+
const redacted = redact(path, input);
|
|
45
|
+
if (!Object.is(redacted, input))
|
|
46
|
+
warnings.add("Capture contains redacted values.");
|
|
47
|
+
input = redacted;
|
|
48
|
+
}
|
|
43
49
|
budget -= 8 + (path.at(-1)?.length ?? 0);
|
|
44
50
|
if (budget <= 0 || depth > 12) {
|
|
45
51
|
warnings.add("Capture size/depth limit reached.");
|