jev-devtools 0.5.0 → 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 +52 -28
- package/lib/flow/index.d.ts +34 -5
- package/lib/flow/index.js +142 -60
- 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 +17 -3
- package/skills/setup-jev-devtools/references/flows.md +189 -112
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
|
|
|
@@ -39,6 +39,8 @@ Install from npm:
|
|
|
39
39
|
pnpm add jev-devtools @typesafe-ai/sdk
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
Use ESM imports, matching React/React DOM **18.x or 19.x**, Node **22.12+**, and TypeSafe SDK **0.7.x**. TypeScript consumers need **5.4+** and compatible React/Node types. React and React DOM are required package peers even for server-only installations. See [compatibility and all public imports](https://github.com/diegoag64/jev-devtools/blob/main/docs/integration.md#compatibility-and-public-imports); `/server` and `/flow` run only on Node.
|
|
43
|
+
|
|
42
44
|
To try local changes before a future release:
|
|
43
45
|
|
|
44
46
|
```sh
|
|
@@ -46,25 +48,25 @@ To try local changes before a future release:
|
|
|
46
48
|
npm ci
|
|
47
49
|
npm run pack:devtools
|
|
48
50
|
# From the consuming app:
|
|
49
|
-
npm install /absolute/path/to/jev-devtools-0.5.
|
|
51
|
+
npm install /absolute/path/to/jev-devtools-0.5.2.tgz
|
|
50
52
|
```
|
|
51
53
|
|
|
52
54
|
Mount once. The drawer loads its own styles; no CSS import is needed:
|
|
53
55
|
|
|
54
56
|
```tsx
|
|
55
|
-
import { JevDevtools } from
|
|
57
|
+
import { JevDevtools } from "jev-devtools";
|
|
56
58
|
|
|
57
|
-
<JevDevtools enabled={import.meta.env.DEV} defaultOpen={false}
|
|
59
|
+
<JevDevtools enabled={import.meta.env.DEV} defaultOpen={false} />;
|
|
58
60
|
```
|
|
59
61
|
|
|
60
62
|
Create one shared Node adapter and instrumented client:
|
|
61
63
|
|
|
62
64
|
```ts
|
|
63
|
-
import { createJevDevtools } from
|
|
65
|
+
import { createJevDevtools } from "jev-devtools/server";
|
|
64
66
|
|
|
65
67
|
const inspector = createJevDevtools({
|
|
66
|
-
enabled: process.env.NODE_ENV ===
|
|
67
|
-
origin:
|
|
68
|
+
enabled: process.env.NODE_ENV === "development",
|
|
69
|
+
origin: "http://localhost:3000",
|
|
68
70
|
});
|
|
69
71
|
const jev = inspector.createClient({ apiKey: process.env.TYPESAFE_API_KEY });
|
|
70
72
|
app.use(inspector.middleware); // Mount at root, before fallback handlers.
|
|
@@ -72,7 +74,7 @@ app.use(inspector.middleware); // Mount at root, before fallback handlers.
|
|
|
72
74
|
// Call inspector.dispose() on shutdown.
|
|
73
75
|
```
|
|
74
76
|
|
|
75
|
-
Only calls through that client are captured. The React component subscribes via same-origin SSE, receiving an initial snapshot followed by changed traces. Older client/server combinations remain compatible through full snapshots. Both sides default to disabled. API keys remain server-side. Browser code is SSR-safe. The drawer uses a Shadow DOM boundary and its own stylesheet so host page selectors cannot alter its internal layout.
|
|
77
|
+
Only calls through that client are captured. The React component subscribes via same-origin SSE, receiving an initial snapshot followed by changed traces. Older client/server combinations remain compatible through full snapshots. Both sides default to disabled. API keys remain server-side. Browser code is SSR-safe. The drawer uses a Shadow DOM boundary and its own stylesheet so host page selectors cannot alter its internal layout. No Tailwind or shadcn installation is required. Drag the Flow canvas to pan, scroll/pinch to zoom, or use Fit.
|
|
76
78
|
|
|
77
79
|
History is bounded to 50 calls in memory by default. API routes and inspector endpoints must share an adapter instance; distributed processes and edge runtimes are not supported. Without `authorize`, the middleware accepts only loopback connections; Web Request handlers can't see the connection's address, so run those servers on `127.0.0.1`. Captures include bounded/redacted context and answers, not hidden model reasoning.
|
|
78
80
|
|
|
@@ -93,27 +95,49 @@ Define calls, conditions, parallel checks, and named outcomes with `jev-devtools
|
|
|
93
95
|
On the server, reuse the instrumented `jev` client and `inspector` from the setup above:
|
|
94
96
|
|
|
95
97
|
```ts
|
|
96
|
-
import { defineJevFlow } from
|
|
97
|
-
|
|
98
|
-
const review = defineJevFlow<{ message: string }>(
|
|
99
|
-
const decision = flow.jev(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
+
);
|
|
111
132
|
});
|
|
112
133
|
|
|
113
|
-
const result = await review.run(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
134
|
+
const result = await review.run(
|
|
135
|
+
{ message: "Please review my ticket." },
|
|
136
|
+
{
|
|
137
|
+
client: jev,
|
|
138
|
+
devtools: inspector,
|
|
139
|
+
},
|
|
140
|
+
);
|
|
117
141
|
// Handle result.outcome in your app. The flow does not execute business actions.
|
|
118
142
|
```
|
|
119
143
|
|
package/lib/flow/index.d.ts
CHANGED
|
@@ -26,7 +26,8 @@ export declare class FlowExpression<T> {
|
|
|
26
26
|
readonly ast: Ast;
|
|
27
27
|
protected readonly [valueTypeBrand]: T;
|
|
28
28
|
constructor(ast: Ast, owner?: object);
|
|
29
|
-
|
|
29
|
+
/** Select an own field from an object or array; primitive properties are unsupported. */
|
|
30
|
+
get<K extends keyof T & (string | number)>(this: FlowExpression<object>, key: K): FlowExpression<T[K]>;
|
|
30
31
|
eq(value: T | FlowExpression<T>): FlowExpression<boolean>;
|
|
31
32
|
lt(this: FlowExpression<number>, value: number | FlowExpression<number>): FlowExpression<boolean>;
|
|
32
33
|
gt(this: FlowExpression<number>, value: number | FlowExpression<number>): FlowExpression<boolean>;
|
|
@@ -52,7 +53,7 @@ type Request<Q extends Questions> = Omit<SystemOneRequest<Q>, "state"> & {
|
|
|
52
53
|
state: unknown;
|
|
53
54
|
};
|
|
54
55
|
/** Resolve nested references before invoking a server callback. */
|
|
55
|
-
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[] ? {
|
|
56
57
|
[K in keyof T]: Resolved<T[K]>;
|
|
57
58
|
} : T extends object ? {
|
|
58
59
|
[K in keyof T]: Resolved<T[K]>;
|
|
@@ -99,9 +100,11 @@ export interface FlowBuilder<T> {
|
|
|
99
100
|
jev<const Q extends Questions>(id: string, request: Request<Q>, display?: FlowDisplay): AnswerRefs<Q>;
|
|
100
101
|
jev<I, const Q extends Questions>(id: string, request: FlowRequestFactory<I, Q>, display?: FlowDisplay): AnswerRefs<Q>;
|
|
101
102
|
task<I, O>(id: string, task: FlowTask<I, O>, display?: FlowDisplay): FlowExpression<Awaited<O>>;
|
|
102
|
-
/**
|
|
103
|
+
/** References and scalar arrays already carry their resolved item type. */
|
|
103
104
|
map<I, C, O>(id: string, options: {
|
|
104
|
-
items: FlowExpression<readonly I[]> | readonly I[]
|
|
105
|
+
items: FlowExpression<readonly I[]> | (readonly I[] & readonly (string | number | {
|
|
106
|
+
valueOf(): boolean;
|
|
107
|
+
} | null)[]);
|
|
105
108
|
input: C;
|
|
106
109
|
concurrency?: number;
|
|
107
110
|
maxItems: number;
|
|
@@ -110,17 +113,43 @@ export interface FlowBuilder<T> {
|
|
|
110
113
|
index: number;
|
|
111
114
|
context: Resolved<C>;
|
|
112
115
|
}>) => FlowExpression<O>, display?: FlowDisplay): FlowExpression<O[]>;
|
|
113
|
-
/**
|
|
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. */
|
|
114
128
|
loop<S, C>(id: string, options: {
|
|
115
129
|
initial: S | FlowExpression<S>;
|
|
116
130
|
input: C;
|
|
117
131
|
while: (state: FlowExpression<NoInfer<S>>) => FlowExpression<boolean>;
|
|
118
132
|
maxIterations: number;
|
|
133
|
+
} & {
|
|
134
|
+
initial: FlowExpression<S> | string | number | {
|
|
135
|
+
valueOf(): boolean;
|
|
136
|
+
} | null;
|
|
119
137
|
}, build: (iteration: FlowBuilder<{
|
|
120
138
|
state: S;
|
|
121
139
|
index: number;
|
|
122
140
|
context: Resolved<C>;
|
|
123
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>>;
|
|
124
153
|
/** Execute one value-returning path, then continue with its typed result. */
|
|
125
154
|
if<A, B>(id: string, paths: {
|
|
126
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 {
|
|
@@ -32,6 +49,7 @@ export class FlowExpression {
|
|
|
32
49
|
freezeAst(ast);
|
|
33
50
|
Object.freeze(this);
|
|
34
51
|
}
|
|
52
|
+
/** Select an own field from an object or array; primitive properties are unsupported. */
|
|
35
53
|
get(key) {
|
|
36
54
|
return expression({ op: "get", from: this.ast, key: String(key) }, [this]);
|
|
37
55
|
}
|
|
@@ -89,7 +107,7 @@ const plain = (v) => !!v &&
|
|
|
89
107
|
typeof v === "object" &&
|
|
90
108
|
(Object.getPrototypeOf(v) === Object.prototype ||
|
|
91
109
|
Object.getPrototypeOf(v) === null);
|
|
92
|
-
function mapValue(value, ref, seen = new Set(), depth = 0) {
|
|
110
|
+
function mapValue(value, ref, rejectSymbolKeys = false, seen = new Set(), depth = 0) {
|
|
93
111
|
if (depth > 40)
|
|
94
112
|
throw new Error("Flow value nesting exceeds 40");
|
|
95
113
|
if (value instanceof FlowExpression)
|
|
@@ -98,14 +116,17 @@ function mapValue(value, ref, seen = new Set(), depth = 0) {
|
|
|
98
116
|
throw new Error("Flow values cannot contain callbacks");
|
|
99
117
|
if (!Array.isArray(value) && !plain(value))
|
|
100
118
|
return value; // Preserve SDK assets.
|
|
119
|
+
// Task templates must reject keys that copying would otherwise silently discard.
|
|
120
|
+
if (rejectSymbolKeys && Object.getOwnPropertySymbols(value).length)
|
|
121
|
+
throw new TypeError("Task data cannot contain symbol keys");
|
|
101
122
|
if (seen.has(value))
|
|
102
123
|
throw new Error("Cyclic flow value");
|
|
103
124
|
seen.add(value);
|
|
104
125
|
const result = Array.isArray(value)
|
|
105
|
-
? value.map((v) => mapValue(v, ref, seen, depth + 1))
|
|
126
|
+
? value.map((v) => mapValue(v, ref, rejectSymbolKeys, seen, depth + 1))
|
|
106
127
|
: Object.fromEntries(Object.entries(value).map(([k, v]) => [
|
|
107
128
|
k,
|
|
108
|
-
mapValue(v, ref, seen, depth + 1),
|
|
129
|
+
mapValue(v, ref, rejectSymbolKeys, seen, depth + 1),
|
|
109
130
|
]));
|
|
110
131
|
seen.delete(value);
|
|
111
132
|
return result;
|
|
@@ -119,21 +140,47 @@ const inspectAst = (ast, ref) => {
|
|
|
119
140
|
ast.args.forEach((a) => inspectAst(a, ref));
|
|
120
141
|
};
|
|
121
142
|
function describe(ast) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return
|
|
130
|
-
|
|
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;
|
|
131
175
|
}
|
|
132
176
|
function conditionRule(ast, requests, tasks) {
|
|
133
177
|
let remaining = 128;
|
|
178
|
+
const warnings = new Set();
|
|
134
179
|
const visit = (node) => {
|
|
135
|
-
if (--remaining < 0)
|
|
180
|
+
if (--remaining < 0) {
|
|
181
|
+
warnings.add("Rule projection limit reached.");
|
|
136
182
|
return { op: "literal", value: "[rule truncated]" };
|
|
183
|
+
}
|
|
137
184
|
if (node.op === "input" || node.op === "ref" || node.op === "get") {
|
|
138
185
|
const path = [];
|
|
139
186
|
let base = node;
|
|
@@ -175,16 +222,20 @@ function conditionRule(ast, requests, tasks) {
|
|
|
175
222
|
: {}),
|
|
176
223
|
};
|
|
177
224
|
}
|
|
178
|
-
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.");
|
|
179
232
|
return {
|
|
180
233
|
op: "literal",
|
|
181
|
-
value:
|
|
182
|
-
typeof node.value === "string" ||
|
|
183
|
-
typeof node.value === "boolean" ||
|
|
184
|
-
(typeof node.value === "number" && Number.isFinite(node.value))
|
|
234
|
+
value: scalar
|
|
185
235
|
? node.value
|
|
186
236
|
: "[value unavailable]",
|
|
187
237
|
};
|
|
238
|
+
}
|
|
188
239
|
const args = [];
|
|
189
240
|
for (const arg of node.args) {
|
|
190
241
|
args.push(visit(arg));
|
|
@@ -193,7 +244,8 @@ function conditionRule(ast, requests, tasks) {
|
|
|
193
244
|
}
|
|
194
245
|
return { op: node.op, args };
|
|
195
246
|
};
|
|
196
|
-
|
|
247
|
+
const rule = visit(ast);
|
|
248
|
+
return { conditionRule: rule, conditionCaptureWarnings: [...warnings] };
|
|
197
249
|
}
|
|
198
250
|
function evaluate(ast, input, values) {
|
|
199
251
|
if (ast.op === "input")
|
|
@@ -380,7 +432,7 @@ export function defineJevFlow(name, build) {
|
|
|
380
432
|
task.timeoutMs < 1 ||
|
|
381
433
|
task.timeoutMs > 2_147_483_647))
|
|
382
434
|
throw new Error("Task timeoutMs must be a positive 32-bit integer");
|
|
383
|
-
const frozen = mapValue(task.input, (expr) => expr);
|
|
435
|
+
const frozen = mapValue(task.input, (expr) => expr, true);
|
|
384
436
|
const step = add(id, "task", frozen, display);
|
|
385
437
|
step.input = frozen;
|
|
386
438
|
step.task = task.run;
|
|
@@ -488,7 +540,7 @@ export function defineJevFlow(name, build) {
|
|
|
488
540
|
return { ...ast, args: ast.args.map(referenceState) };
|
|
489
541
|
return ast;
|
|
490
542
|
};
|
|
491
|
-
node
|
|
543
|
+
Object.assign(node, conditionRule(referenceState(when.ast), new Set(), new Set([id])));
|
|
492
544
|
return new FlowExpression({ op: "ref", id }, owner);
|
|
493
545
|
},
|
|
494
546
|
if(id, paths, display) {
|
|
@@ -498,9 +550,11 @@ export function defineJevFlow(name, build) {
|
|
|
498
550
|
step.when = paths.when;
|
|
499
551
|
const node = nodes.find((n) => n.id === id);
|
|
500
552
|
node.condition = describe(paths.when.ast);
|
|
501
|
-
node
|
|
502
|
-
.filter((n) => n.kind === "task" ||
|
|
503
|
-
.
|
|
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))));
|
|
504
558
|
step.then = block(paths.then, new Set(available), [...path, `${id}:then`], [id], depth + 1, false, true);
|
|
505
559
|
step.else = block(paths.else, new Set(available), [...path, `${id}:else`], [id], depth + 1, false, true);
|
|
506
560
|
// A separate merge keeps the diagram acyclic and gives downstream refs a
|
|
@@ -530,9 +584,11 @@ export function defineJevFlow(name, build) {
|
|
|
530
584
|
throw new Error("branch requires a condition expression");
|
|
531
585
|
const node = nodes.find((n) => n.id === id);
|
|
532
586
|
node.condition = describe(paths.when.ast);
|
|
533
|
-
node
|
|
534
|
-
.filter((n) => n.kind === "task" ||
|
|
535
|
-
.
|
|
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))));
|
|
536
592
|
step.then = block(paths.then, new Set(available), [...path, `${id}:then`], [id], depth + 1, true);
|
|
537
593
|
step.else = block(paths.else, new Set(available), [...path, `${id}:else`], [id], depth + 1, true);
|
|
538
594
|
terminal = true;
|
|
@@ -620,6 +676,7 @@ export function defineJevFlow(name, build) {
|
|
|
620
676
|
if (node.lanes)
|
|
621
677
|
Object.freeze(node.lanes);
|
|
622
678
|
freezeRule(node.conditionRule);
|
|
679
|
+
freezeRule(node.conditionCaptureWarnings);
|
|
623
680
|
freezeRule(node.itemDefinition);
|
|
624
681
|
Object.freeze(node);
|
|
625
682
|
}
|
|
@@ -680,49 +737,73 @@ export function defineJevFlow(name, build) {
|
|
|
680
737
|
if (value && typeof value === "object" && observerData.has(value))
|
|
681
738
|
return observerData.get(value);
|
|
682
739
|
let snapshot;
|
|
740
|
+
const warnings = [];
|
|
683
741
|
try {
|
|
684
742
|
snapshot = observerSnapshot(value);
|
|
685
743
|
}
|
|
686
744
|
catch {
|
|
687
745
|
// Direct flow outputs may be non-JSON; only their capture is unavailable.
|
|
688
746
|
snapshot = "[capture unavailable]";
|
|
747
|
+
warnings.push("Observer capture unavailable.");
|
|
689
748
|
}
|
|
749
|
+
const captured = { data: snapshot, warnings };
|
|
690
750
|
if (value && typeof value === "object")
|
|
691
|
-
observerData.set(value,
|
|
692
|
-
return
|
|
751
|
+
observerData.set(value, captured);
|
|
752
|
+
return captured;
|
|
693
753
|
};
|
|
694
754
|
const emit = () => {
|
|
695
755
|
try {
|
|
696
756
|
if (observer) {
|
|
697
757
|
const { value, steps, ...metadata } = run;
|
|
758
|
+
const outcome = value !== undefined ? snapshotData(value) : undefined;
|
|
698
759
|
observer.update({
|
|
699
760
|
...structuredClone(metadata),
|
|
700
|
-
steps: Object.fromEntries(Object.entries(steps).map(([id, step]) =>
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
? {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
+
: {}),
|
|
726
807
|
});
|
|
727
808
|
}
|
|
728
809
|
}
|
|
@@ -807,7 +888,7 @@ export function defineJevFlow(name, build) {
|
|
|
807
888
|
abortCheck();
|
|
808
889
|
}
|
|
809
890
|
else if (step.kind === "task") {
|
|
810
|
-
const input = jsonSnapshot(mapValue(step.input, (e) => evaluate(e.ast, data, values)));
|
|
891
|
+
const input = jsonSnapshot(mapValue(step.input, (e) => evaluate(e.ast, data, values), true));
|
|
811
892
|
state.data = { input };
|
|
812
893
|
emit();
|
|
813
894
|
value = jsonSnapshot(await runCallback((signal) => step.task(jsonSnapshot(input), {
|
|
@@ -1000,6 +1081,7 @@ export function defineJevFlow(name, build) {
|
|
|
1000
1081
|
}
|
|
1001
1082
|
catch {
|
|
1002
1083
|
state.data = "Selected value could not be captured as JSON.";
|
|
1084
|
+
state.captureWarnings = ["Selected value capture unavailable."];
|
|
1003
1085
|
}
|
|
1004
1086
|
}
|
|
1005
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.");
|