deepline 0.2.53 → 0.2.55
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/dist/bundling-sources/sdk/src/client.ts +17 -1
- package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +1 -1
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/types.ts +43 -1
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +30 -1
- package/dist/bundling-sources/shared_libs/play-runtime/cell-provenance.ts +231 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1094 -128
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +178 -9
- package/dist/bundling-sources/shared_libs/play-runtime/docflow-node-io.ts +634 -0
- package/dist/bundling-sources/shared_libs/play-runtime/docflow-observation.ts +64 -0
- package/dist/bundling-sources/shared_libs/play-runtime/dynamic-worker-version.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +18 -0
- package/dist/bundling-sources/shared_libs/play-runtime/live-state-contract.ts +33 -0
- package/dist/bundling-sources/shared_libs/play-runtime/log-provenance.ts +251 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-node-scope.ts +160 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +27 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +43 -5
- package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +12 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +26 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +6 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +83 -0
- package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +3 -0
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +49 -1
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +375 -29
- package/dist/bundling-sources/shared_libs/plays/docflow-binding-owner.ts +636 -0
- package/dist/bundling-sources/shared_libs/plays/docflow-binding.ts +598 -0
- package/dist/bundling-sources/shared_libs/plays/docflow.ts +1645 -0
- package/dist/bundling-sources/shared_libs/plays/play-exports.ts +202 -0
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +16 -1
- package/dist/bundling-sources/shared_libs/plays/ts-ast.ts +48 -0
- package/dist/cli/index.js +994 -312
- package/dist/cli/index.mjs +994 -312
- package/dist/{compiler-manifest-Cj3--4ZJ.d.mts → compiler-manifest-Bl8kmLx9.d.mts} +118 -0
- package/dist/{compiler-manifest-Cj3--4ZJ.d.ts → compiler-manifest-Bl8kmLx9.d.ts} +118 -0
- package/dist/index.d.mts +47 -2
- package/dist/index.d.ts +47 -2
- package/dist/index.js +419 -59
- package/dist/index.mjs +419 -59
- package/dist/install-integrity.json +12 -2
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/bundle-play-file.mjs +1361 -45
- package/package.json +1 -1
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSecretRedactionContext,
|
|
3
|
+
type SecretRedactionContext,
|
|
4
|
+
} from './secret-redaction';
|
|
5
|
+
|
|
6
|
+
export const DOCFLOW_NODE_IO_LIMITS = {
|
|
7
|
+
maxPaths: 16,
|
|
8
|
+
maxDepth: 3,
|
|
9
|
+
maxObjectFields: 12,
|
|
10
|
+
maxArrayItems: 3,
|
|
11
|
+
maxStringBytes: 256,
|
|
12
|
+
maxPhaseBytes: 2_000,
|
|
13
|
+
maxErrorBytes: 512,
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export type PlayDocflowNodeValuePreview =
|
|
17
|
+
| { kind: 'null' }
|
|
18
|
+
| { kind: 'scalar'; value: string | number | boolean }
|
|
19
|
+
| {
|
|
20
|
+
kind: 'dataset';
|
|
21
|
+
datasetId: string;
|
|
22
|
+
datasetKind: 'csv' | 'map';
|
|
23
|
+
tableNamespace?: string | null;
|
|
24
|
+
rowCount?: number;
|
|
25
|
+
}
|
|
26
|
+
| {
|
|
27
|
+
kind: 'array';
|
|
28
|
+
items: PlayDocflowNodeValuePreview[];
|
|
29
|
+
totalItems: number;
|
|
30
|
+
truncated?: boolean;
|
|
31
|
+
}
|
|
32
|
+
| {
|
|
33
|
+
kind: 'object';
|
|
34
|
+
fields: Record<string, PlayDocflowNodeValuePreview>;
|
|
35
|
+
totalFields: number;
|
|
36
|
+
truncated?: boolean;
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
kind: 'unavailable';
|
|
40
|
+
reason:
|
|
41
|
+
| 'undefined'
|
|
42
|
+
| 'function'
|
|
43
|
+
| 'symbol'
|
|
44
|
+
| 'bigint'
|
|
45
|
+
| 'cycle'
|
|
46
|
+
| 'depth'
|
|
47
|
+
| 'access_error'
|
|
48
|
+
| 'limit'
|
|
49
|
+
| 'unsupported';
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type PlayDocflowNodeIoPreviewMap = Record<
|
|
53
|
+
string,
|
|
54
|
+
PlayDocflowNodeValuePreview
|
|
55
|
+
>;
|
|
56
|
+
|
|
57
|
+
export type PlayDocflowNodeInputCapture = {
|
|
58
|
+
path: string;
|
|
59
|
+
/** Reads only the lexical root. TDZ and missing bindings are contained. */
|
|
60
|
+
readRoot: () => unknown;
|
|
61
|
+
/** Traversed through data descriptors so authored getters are never invoked. */
|
|
62
|
+
properties: readonly string[];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type PlayDocflowNodeIoState = {
|
|
66
|
+
attempt: number;
|
|
67
|
+
invocationId?: string;
|
|
68
|
+
inputs?: PlayDocflowNodeIoPreviewMap;
|
|
69
|
+
outputs?: PlayDocflowNodeIoPreviewMap;
|
|
70
|
+
inputsTruncated?: boolean;
|
|
71
|
+
outputsTruncated?: boolean;
|
|
72
|
+
error?: string | null;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type PreviewOptions = {
|
|
76
|
+
redactor?: SecretRedactionContext;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const utf8Encoder = new TextEncoder();
|
|
80
|
+
const PLAY_DATASET_BRAND = Symbol.for('deepline.play.dataset');
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Preview-map keys must be persistable everywhere the ledger lands, and Convex
|
|
84
|
+
* reserves field names beginning with '$' (a terminal flush carrying one 500s,
|
|
85
|
+
* dropping the WHOLE terminal event batch). `$output` is the wrapper's
|
|
86
|
+
* whole-result path; strip the sigil so previews survive every store. Applied
|
|
87
|
+
* at every build/normalize seam so live events, durable observations, and
|
|
88
|
+
* replayed historical payloads all converge on the safe representation.
|
|
89
|
+
*/
|
|
90
|
+
export function ledgerSafeDocflowPreviewKey(key: string): string {
|
|
91
|
+
if (!key.startsWith('$')) return key;
|
|
92
|
+
const stripped = key.replace(/^\$+/, '');
|
|
93
|
+
return stripped || 'output';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function utf8Bytes(value: string): number {
|
|
97
|
+
return utf8Encoder.encode(value).byteLength;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function truncateUtf8(value: string, maxBytes: number): string {
|
|
101
|
+
if (utf8Bytes(value) <= maxBytes) return value;
|
|
102
|
+
let output = '';
|
|
103
|
+
for (const character of value) {
|
|
104
|
+
if (utf8Bytes(`${output}${character}…`) > maxBytes) break;
|
|
105
|
+
output += character;
|
|
106
|
+
}
|
|
107
|
+
return `${output}…`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function redactString(
|
|
111
|
+
redactor: SecretRedactionContext,
|
|
112
|
+
value: string,
|
|
113
|
+
key?: string,
|
|
114
|
+
): string {
|
|
115
|
+
if (!key) return redactor.redactString(value);
|
|
116
|
+
const wrapped = redactor.redact({ [key]: value }) as Record<string, unknown>;
|
|
117
|
+
return typeof wrapped[key] === 'string'
|
|
118
|
+
? wrapped[key]
|
|
119
|
+
: redactor.redactString(value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function safeString(
|
|
123
|
+
value: unknown,
|
|
124
|
+
redactor: SecretRedactionContext,
|
|
125
|
+
key?: string,
|
|
126
|
+
): string | undefined {
|
|
127
|
+
if (typeof value !== 'string') return undefined;
|
|
128
|
+
return truncateUtf8(
|
|
129
|
+
redactString(redactor, value, key),
|
|
130
|
+
DOCFLOW_NODE_IO_LIMITS.maxStringBytes,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function previewValue(
|
|
135
|
+
value: unknown,
|
|
136
|
+
input: {
|
|
137
|
+
depth: number;
|
|
138
|
+
ancestors: WeakSet<object>;
|
|
139
|
+
redactor: SecretRedactionContext;
|
|
140
|
+
key?: string;
|
|
141
|
+
},
|
|
142
|
+
): PlayDocflowNodeValuePreview {
|
|
143
|
+
if (value === null) return { kind: 'null' };
|
|
144
|
+
if (value === undefined) return { kind: 'unavailable', reason: 'undefined' };
|
|
145
|
+
if (typeof value === 'string') {
|
|
146
|
+
return {
|
|
147
|
+
kind: 'scalar',
|
|
148
|
+
value: safeString(value, input.redactor, input.key) ?? '',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (typeof value === 'number') {
|
|
152
|
+
return Number.isFinite(value)
|
|
153
|
+
? { kind: 'scalar', value }
|
|
154
|
+
: { kind: 'unavailable', reason: 'unsupported' };
|
|
155
|
+
}
|
|
156
|
+
if (typeof value === 'boolean') return { kind: 'scalar', value };
|
|
157
|
+
if (typeof value === 'bigint') {
|
|
158
|
+
return { kind: 'unavailable', reason: 'bigint' };
|
|
159
|
+
}
|
|
160
|
+
if (typeof value === 'function') {
|
|
161
|
+
return { kind: 'unavailable', reason: 'function' };
|
|
162
|
+
}
|
|
163
|
+
if (typeof value === 'symbol') {
|
|
164
|
+
return { kind: 'unavailable', reason: 'symbol' };
|
|
165
|
+
}
|
|
166
|
+
if (typeof value !== 'object') {
|
|
167
|
+
return { kind: 'unavailable', reason: 'unsupported' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const brand = readDataProperty(value, PLAY_DATASET_BRAND);
|
|
172
|
+
const datasetKind = readDataProperty(value, 'datasetKind');
|
|
173
|
+
const datasetId = readDataProperty(value, 'datasetId');
|
|
174
|
+
const tableNamespace = readDataProperty(value, 'tableNamespace');
|
|
175
|
+
if (
|
|
176
|
+
brand.ok &&
|
|
177
|
+
brand.value === true &&
|
|
178
|
+
datasetKind.ok &&
|
|
179
|
+
(datasetKind.value === 'csv' || datasetKind.value === 'map') &&
|
|
180
|
+
datasetId.ok &&
|
|
181
|
+
typeof datasetId.value === 'string'
|
|
182
|
+
) {
|
|
183
|
+
return {
|
|
184
|
+
kind: 'dataset',
|
|
185
|
+
datasetId:
|
|
186
|
+
safeString(datasetId.value, input.redactor, 'datasetId') ?? 'dataset',
|
|
187
|
+
datasetKind: datasetKind.value,
|
|
188
|
+
tableNamespace:
|
|
189
|
+
tableNamespace.ok && tableNamespace.value === null
|
|
190
|
+
? null
|
|
191
|
+
: tableNamespace.ok
|
|
192
|
+
? safeString(
|
|
193
|
+
tableNamespace.value,
|
|
194
|
+
input.redactor,
|
|
195
|
+
'tableNamespace',
|
|
196
|
+
)
|
|
197
|
+
: undefined,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const kind = readDataProperty(value, 'kind');
|
|
201
|
+
const count = readDataProperty(value, 'count');
|
|
202
|
+
const preview = readDataProperty(value, 'preview');
|
|
203
|
+
if (
|
|
204
|
+
kind.ok &&
|
|
205
|
+
kind.value === 'dataset' &&
|
|
206
|
+
datasetKind.ok &&
|
|
207
|
+
(datasetKind.value === 'csv' || datasetKind.value === 'map') &&
|
|
208
|
+
datasetId.ok &&
|
|
209
|
+
typeof datasetId.value === 'string' &&
|
|
210
|
+
count.ok &&
|
|
211
|
+
typeof count.value === 'number' &&
|
|
212
|
+
preview.ok &&
|
|
213
|
+
Array.isArray(preview.value)
|
|
214
|
+
) {
|
|
215
|
+
return {
|
|
216
|
+
kind: 'dataset',
|
|
217
|
+
datasetId:
|
|
218
|
+
safeString(datasetId.value, input.redactor, 'datasetId') ?? 'dataset',
|
|
219
|
+
datasetKind: datasetKind.value,
|
|
220
|
+
tableNamespace:
|
|
221
|
+
tableNamespace.ok && tableNamespace.value === null
|
|
222
|
+
? null
|
|
223
|
+
: tableNamespace.ok
|
|
224
|
+
? safeString(
|
|
225
|
+
tableNamespace.value,
|
|
226
|
+
input.redactor,
|
|
227
|
+
'tableNamespace',
|
|
228
|
+
)
|
|
229
|
+
: undefined,
|
|
230
|
+
rowCount: Math.max(0, Math.floor(count.value)),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
return { kind: 'unavailable', reason: 'access_error' };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (input.ancestors.has(value)) {
|
|
238
|
+
return { kind: 'unavailable', reason: 'cycle' };
|
|
239
|
+
}
|
|
240
|
+
if (input.depth >= DOCFLOW_NODE_IO_LIMITS.maxDepth) {
|
|
241
|
+
return { kind: 'unavailable', reason: 'depth' };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
input.ancestors.add(value);
|
|
245
|
+
try {
|
|
246
|
+
if (Array.isArray(value)) {
|
|
247
|
+
const items: PlayDocflowNodeValuePreview[] = [];
|
|
248
|
+
for (
|
|
249
|
+
let index = 0;
|
|
250
|
+
index < Math.min(value.length, DOCFLOW_NODE_IO_LIMITS.maxArrayItems);
|
|
251
|
+
index += 1
|
|
252
|
+
) {
|
|
253
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
254
|
+
value,
|
|
255
|
+
String(index),
|
|
256
|
+
);
|
|
257
|
+
items.push(
|
|
258
|
+
descriptor && 'value' in descriptor
|
|
259
|
+
? previewValue(descriptor.value, {
|
|
260
|
+
...input,
|
|
261
|
+
depth: input.depth + 1,
|
|
262
|
+
})
|
|
263
|
+
: { kind: 'unavailable', reason: 'access_error' },
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
kind: 'array',
|
|
268
|
+
items,
|
|
269
|
+
totalItems: value.length,
|
|
270
|
+
...(value.length > items.length ? { truncated: true } : {}),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let descriptors: Record<string, PropertyDescriptor>;
|
|
275
|
+
try {
|
|
276
|
+
descriptors = Object.getOwnPropertyDescriptors(value);
|
|
277
|
+
} catch {
|
|
278
|
+
return { kind: 'unavailable', reason: 'access_error' };
|
|
279
|
+
}
|
|
280
|
+
const entries = Object.entries(descriptors).filter(
|
|
281
|
+
([, descriptor]) => descriptor.enumerable,
|
|
282
|
+
);
|
|
283
|
+
const selected = entries.slice(0, DOCFLOW_NODE_IO_LIMITS.maxObjectFields);
|
|
284
|
+
const fields: Record<string, PlayDocflowNodeValuePreview> = {};
|
|
285
|
+
for (const [key, descriptor] of selected) {
|
|
286
|
+
const safeKey = truncateUtf8(
|
|
287
|
+
ledgerSafeDocflowPreviewKey(input.redactor.redactString(key)),
|
|
288
|
+
DOCFLOW_NODE_IO_LIMITS.maxStringBytes,
|
|
289
|
+
);
|
|
290
|
+
fields[safeKey] =
|
|
291
|
+
'value' in descriptor
|
|
292
|
+
? previewValue(descriptor.value, {
|
|
293
|
+
...input,
|
|
294
|
+
depth: input.depth + 1,
|
|
295
|
+
key,
|
|
296
|
+
})
|
|
297
|
+
: { kind: 'unavailable', reason: 'access_error' };
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
kind: 'object',
|
|
301
|
+
fields,
|
|
302
|
+
totalFields: entries.length,
|
|
303
|
+
...(entries.length > selected.length ? { truncated: true } : {}),
|
|
304
|
+
};
|
|
305
|
+
} catch {
|
|
306
|
+
return { kind: 'unavailable', reason: 'access_error' };
|
|
307
|
+
} finally {
|
|
308
|
+
input.ancestors.delete(value);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function buildPlayDocflowNodeIoPreviewMap(
|
|
313
|
+
values: Record<string, unknown>,
|
|
314
|
+
options: PreviewOptions = {},
|
|
315
|
+
): { values: PlayDocflowNodeIoPreviewMap; truncated: boolean } {
|
|
316
|
+
const redactor = options.redactor ?? createSecretRedactionContext();
|
|
317
|
+
const previews: PlayDocflowNodeIoPreviewMap = {};
|
|
318
|
+
let truncated = false;
|
|
319
|
+
let entries: Array<[string, unknown]>;
|
|
320
|
+
try {
|
|
321
|
+
entries = Object.entries(values);
|
|
322
|
+
} catch {
|
|
323
|
+
return {
|
|
324
|
+
values: { value: { kind: 'unavailable', reason: 'access_error' } },
|
|
325
|
+
truncated: true,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
for (const [path, value] of entries) {
|
|
329
|
+
if (Object.keys(previews).length >= DOCFLOW_NODE_IO_LIMITS.maxPaths) {
|
|
330
|
+
truncated = true;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
const safePath = truncateUtf8(
|
|
334
|
+
ledgerSafeDocflowPreviewKey(redactor.redactString(path)),
|
|
335
|
+
DOCFLOW_NODE_IO_LIMITS.maxStringBytes,
|
|
336
|
+
);
|
|
337
|
+
const preview = previewValue(value, {
|
|
338
|
+
depth: 0,
|
|
339
|
+
ancestors: new WeakSet(),
|
|
340
|
+
redactor,
|
|
341
|
+
key: path.split('.').at(-1),
|
|
342
|
+
});
|
|
343
|
+
const candidate = { ...previews, [safePath]: preview };
|
|
344
|
+
if (
|
|
345
|
+
utf8Bytes(JSON.stringify(candidate)) >
|
|
346
|
+
DOCFLOW_NODE_IO_LIMITS.maxPhaseBytes
|
|
347
|
+
) {
|
|
348
|
+
truncated = true;
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
previews[safePath] = preview;
|
|
352
|
+
}
|
|
353
|
+
if (entries.length > Object.keys(previews).length) truncated = true;
|
|
354
|
+
return { values: previews, truncated };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function readDataProperty(
|
|
358
|
+
value: unknown,
|
|
359
|
+
property: PropertyKey,
|
|
360
|
+
): {
|
|
361
|
+
ok: boolean;
|
|
362
|
+
value?: unknown;
|
|
363
|
+
} {
|
|
364
|
+
if (
|
|
365
|
+
(typeof value !== 'object' || value === null) &&
|
|
366
|
+
typeof value !== 'function'
|
|
367
|
+
) {
|
|
368
|
+
return { ok: false };
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
let owner: object | null = value as object;
|
|
372
|
+
while (owner) {
|
|
373
|
+
const descriptor = Object.getOwnPropertyDescriptor(owner, property);
|
|
374
|
+
if (descriptor) {
|
|
375
|
+
return 'value' in descriptor
|
|
376
|
+
? { ok: true, value: descriptor.value }
|
|
377
|
+
: { ok: false };
|
|
378
|
+
}
|
|
379
|
+
owner = Object.getPrototypeOf(owner) as object | null;
|
|
380
|
+
}
|
|
381
|
+
} catch {
|
|
382
|
+
return { ok: false };
|
|
383
|
+
}
|
|
384
|
+
return { ok: false };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function buildPlayDocflowNodeInputPreviewMap(
|
|
388
|
+
captures: readonly PlayDocflowNodeInputCapture[],
|
|
389
|
+
options: PreviewOptions = {},
|
|
390
|
+
): { values: PlayDocflowNodeIoPreviewMap; truncated: boolean } {
|
|
391
|
+
const redactor = options.redactor ?? createSecretRedactionContext();
|
|
392
|
+
const values: PlayDocflowNodeIoPreviewMap = {};
|
|
393
|
+
let truncated = false;
|
|
394
|
+
for (const capture of captures.slice(0, DOCFLOW_NODE_IO_LIMITS.maxPaths)) {
|
|
395
|
+
let current: unknown;
|
|
396
|
+
let available = true;
|
|
397
|
+
try {
|
|
398
|
+
current = capture.readRoot();
|
|
399
|
+
} catch {
|
|
400
|
+
available = false;
|
|
401
|
+
}
|
|
402
|
+
if (available) {
|
|
403
|
+
for (const property of capture.properties) {
|
|
404
|
+
const next = readDataProperty(current, property);
|
|
405
|
+
if (!next.ok) {
|
|
406
|
+
available = false;
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
current = next.value;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const safePath = truncateUtf8(
|
|
413
|
+
ledgerSafeDocflowPreviewKey(redactor.redactString(capture.path)),
|
|
414
|
+
DOCFLOW_NODE_IO_LIMITS.maxStringBytes,
|
|
415
|
+
);
|
|
416
|
+
const preview: PlayDocflowNodeValuePreview = available
|
|
417
|
+
? previewValue(current, {
|
|
418
|
+
depth: 0,
|
|
419
|
+
ancestors: new WeakSet(),
|
|
420
|
+
redactor,
|
|
421
|
+
key: capture.properties.at(-1) ?? capture.path,
|
|
422
|
+
})
|
|
423
|
+
: { kind: 'unavailable', reason: 'access_error' };
|
|
424
|
+
const candidate = { ...values, [safePath]: preview };
|
|
425
|
+
if (
|
|
426
|
+
utf8Bytes(JSON.stringify(candidate)) >
|
|
427
|
+
DOCFLOW_NODE_IO_LIMITS.maxPhaseBytes
|
|
428
|
+
) {
|
|
429
|
+
truncated = true;
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
values[safePath] = preview;
|
|
433
|
+
}
|
|
434
|
+
if (captures.length > Object.keys(values).length) truncated = true;
|
|
435
|
+
return { values, truncated };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function buildPlayDocflowNodeErrorPreview(
|
|
439
|
+
error: unknown,
|
|
440
|
+
redactor: SecretRedactionContext = createSecretRedactionContext(),
|
|
441
|
+
): string {
|
|
442
|
+
try {
|
|
443
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
444
|
+
return truncateUtf8(
|
|
445
|
+
redactor.redactString(message),
|
|
446
|
+
DOCFLOW_NODE_IO_LIMITS.maxErrorBytes,
|
|
447
|
+
);
|
|
448
|
+
} catch {
|
|
449
|
+
return 'Non-Error value thrown (message unavailable)';
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
454
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function normalizePreview(
|
|
458
|
+
value: unknown,
|
|
459
|
+
depth: number,
|
|
460
|
+
): PlayDocflowNodeValuePreview | null {
|
|
461
|
+
if (!isRecord(value) || typeof value.kind !== 'string') return null;
|
|
462
|
+
if (value.kind === 'null') return { kind: 'null' };
|
|
463
|
+
if (value.kind === 'scalar') {
|
|
464
|
+
if (
|
|
465
|
+
typeof value.value === 'string' ||
|
|
466
|
+
typeof value.value === 'boolean' ||
|
|
467
|
+
(typeof value.value === 'number' && Number.isFinite(value.value))
|
|
468
|
+
) {
|
|
469
|
+
return previewValue(value.value, {
|
|
470
|
+
depth: 0,
|
|
471
|
+
ancestors: new WeakSet(),
|
|
472
|
+
redactor: createSecretRedactionContext(),
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
if (value.kind === 'unavailable') {
|
|
478
|
+
const reason = value.reason;
|
|
479
|
+
return typeof reason === 'string' &&
|
|
480
|
+
[
|
|
481
|
+
'undefined',
|
|
482
|
+
'function',
|
|
483
|
+
'symbol',
|
|
484
|
+
'bigint',
|
|
485
|
+
'cycle',
|
|
486
|
+
'depth',
|
|
487
|
+
'access_error',
|
|
488
|
+
'limit',
|
|
489
|
+
'unsupported',
|
|
490
|
+
].includes(reason)
|
|
491
|
+
? {
|
|
492
|
+
kind: 'unavailable',
|
|
493
|
+
reason: reason as Extract<
|
|
494
|
+
PlayDocflowNodeValuePreview,
|
|
495
|
+
{ kind: 'unavailable' }
|
|
496
|
+
>['reason'],
|
|
497
|
+
}
|
|
498
|
+
: null;
|
|
499
|
+
}
|
|
500
|
+
if (value.kind === 'dataset') {
|
|
501
|
+
const datasetId = safeString(
|
|
502
|
+
value.datasetId,
|
|
503
|
+
createSecretRedactionContext(),
|
|
504
|
+
'datasetId',
|
|
505
|
+
);
|
|
506
|
+
if (
|
|
507
|
+
!datasetId ||
|
|
508
|
+
(value.datasetKind !== 'csv' && value.datasetKind !== 'map')
|
|
509
|
+
) {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
kind: 'dataset',
|
|
514
|
+
datasetId,
|
|
515
|
+
datasetKind: value.datasetKind,
|
|
516
|
+
...(value.tableNamespace === null
|
|
517
|
+
? { tableNamespace: null }
|
|
518
|
+
: typeof value.tableNamespace === 'string'
|
|
519
|
+
? {
|
|
520
|
+
tableNamespace: safeString(
|
|
521
|
+
value.tableNamespace,
|
|
522
|
+
createSecretRedactionContext(),
|
|
523
|
+
'tableNamespace',
|
|
524
|
+
),
|
|
525
|
+
}
|
|
526
|
+
: {}),
|
|
527
|
+
...(typeof value.rowCount === 'number' && Number.isFinite(value.rowCount)
|
|
528
|
+
? { rowCount: Math.max(0, Math.floor(value.rowCount)) }
|
|
529
|
+
: {}),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
if (depth >= DOCFLOW_NODE_IO_LIMITS.maxDepth) {
|
|
533
|
+
return { kind: 'unavailable', reason: 'depth' };
|
|
534
|
+
}
|
|
535
|
+
if (value.kind === 'array' && Array.isArray(value.items)) {
|
|
536
|
+
const items = value.items
|
|
537
|
+
.slice(0, DOCFLOW_NODE_IO_LIMITS.maxArrayItems)
|
|
538
|
+
.map((item) => normalizePreview(item, depth + 1))
|
|
539
|
+
.filter((item): item is PlayDocflowNodeValuePreview => item !== null);
|
|
540
|
+
const totalItems =
|
|
541
|
+
typeof value.totalItems === 'number' && Number.isFinite(value.totalItems)
|
|
542
|
+
? Math.max(items.length, Math.floor(value.totalItems))
|
|
543
|
+
: items.length;
|
|
544
|
+
return {
|
|
545
|
+
kind: 'array',
|
|
546
|
+
items,
|
|
547
|
+
totalItems,
|
|
548
|
+
...(value.truncated === true || totalItems > items.length
|
|
549
|
+
? { truncated: true }
|
|
550
|
+
: {}),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
if (value.kind === 'object' && isRecord(value.fields)) {
|
|
554
|
+
const fields: Record<string, PlayDocflowNodeValuePreview> = {};
|
|
555
|
+
for (const [key, field] of Object.entries(value.fields).slice(
|
|
556
|
+
0,
|
|
557
|
+
DOCFLOW_NODE_IO_LIMITS.maxObjectFields,
|
|
558
|
+
)) {
|
|
559
|
+
const normalized = normalizePreview(field, depth + 1);
|
|
560
|
+
if (normalized) fields[ledgerSafeDocflowPreviewKey(key)] = normalized;
|
|
561
|
+
}
|
|
562
|
+
const totalFields =
|
|
563
|
+
typeof value.totalFields === 'number' &&
|
|
564
|
+
Number.isFinite(value.totalFields)
|
|
565
|
+
? Math.max(Object.keys(fields).length, Math.floor(value.totalFields))
|
|
566
|
+
: Object.keys(fields).length;
|
|
567
|
+
return {
|
|
568
|
+
kind: 'object',
|
|
569
|
+
fields,
|
|
570
|
+
totalFields,
|
|
571
|
+
...(value.truncated === true || totalFields > Object.keys(fields).length
|
|
572
|
+
? { truncated: true }
|
|
573
|
+
: {}),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export function normalizePlayDocflowNodeIoState(
|
|
580
|
+
value: unknown,
|
|
581
|
+
): PlayDocflowNodeIoState | undefined {
|
|
582
|
+
if (!isRecord(value)) return undefined;
|
|
583
|
+
const attempt =
|
|
584
|
+
typeof value.attempt === 'number' && Number.isFinite(value.attempt)
|
|
585
|
+
? Math.max(0, Math.floor(value.attempt))
|
|
586
|
+
: 0;
|
|
587
|
+
const normalizeMap = (
|
|
588
|
+
raw: unknown,
|
|
589
|
+
): PlayDocflowNodeIoPreviewMap | undefined => {
|
|
590
|
+
if (!isRecord(raw)) return undefined;
|
|
591
|
+
const output: PlayDocflowNodeIoPreviewMap = {};
|
|
592
|
+
for (const [path, preview] of Object.entries(raw).slice(
|
|
593
|
+
0,
|
|
594
|
+
DOCFLOW_NODE_IO_LIMITS.maxPaths,
|
|
595
|
+
)) {
|
|
596
|
+
const normalized = normalizePreview(preview, 0);
|
|
597
|
+
if (!normalized) continue;
|
|
598
|
+
const safePath = ledgerSafeDocflowPreviewKey(path);
|
|
599
|
+
const candidate = { ...output, [safePath]: normalized };
|
|
600
|
+
if (
|
|
601
|
+
utf8Bytes(JSON.stringify(candidate)) >
|
|
602
|
+
DOCFLOW_NODE_IO_LIMITS.maxPhaseBytes
|
|
603
|
+
) {
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
output[safePath] = normalized;
|
|
607
|
+
}
|
|
608
|
+
return output;
|
|
609
|
+
};
|
|
610
|
+
const inputs = normalizeMap(value.inputs);
|
|
611
|
+
const outputs = normalizeMap(value.outputs);
|
|
612
|
+
const error =
|
|
613
|
+
typeof value.error === 'string'
|
|
614
|
+
? buildPlayDocflowNodeErrorPreview(value.error)
|
|
615
|
+
: value.error === null
|
|
616
|
+
? null
|
|
617
|
+
: undefined;
|
|
618
|
+
return {
|
|
619
|
+
attempt,
|
|
620
|
+
...(typeof value.invocationId === 'string' && value.invocationId.trim()
|
|
621
|
+
? {
|
|
622
|
+
invocationId: truncateUtf8(
|
|
623
|
+
value.invocationId.trim(),
|
|
624
|
+
DOCFLOW_NODE_IO_LIMITS.maxStringBytes,
|
|
625
|
+
),
|
|
626
|
+
}
|
|
627
|
+
: {}),
|
|
628
|
+
...(inputs ? { inputs } : {}),
|
|
629
|
+
...(outputs ? { outputs } : {}),
|
|
630
|
+
...(value.inputsTruncated === true ? { inputsTruncated: true } : {}),
|
|
631
|
+
...(value.outputsTruncated === true ? { outputsTruncated: true } : {}),
|
|
632
|
+
...(error !== undefined ? { error } : {}),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable docflow observation record (ADR 0016 rule 2).
|
|
3
|
+
*
|
|
4
|
+
* An observation is one authored-node invocation recorded on the same durable
|
|
5
|
+
* wire receipts use (the runtime receipt gateway) into the scheduler schema's
|
|
6
|
+
* `observations` table. It is keyed like a receipt — org/run scope proven by the
|
|
7
|
+
* executor token, fenced by the per-attempt authority — plus the node id and the
|
|
8
|
+
* wrapper's per-attempt invocation sequence.
|
|
9
|
+
*
|
|
10
|
+
* Observations are a projection of the live `docflow.node.*` execution events
|
|
11
|
+
* onto durable storage; the wrapper still emits those events for live streaming.
|
|
12
|
+
* They carry only BOUNDED input/output previews (per `docflow-node-io`), never
|
|
13
|
+
* per-row map truth — maps observe at the node level while per-row outcomes live
|
|
14
|
+
* in the sheets.
|
|
15
|
+
*/
|
|
16
|
+
import type { PlayDocflowNodeIoPreviewMap } from './docflow-node-io';
|
|
17
|
+
|
|
18
|
+
export type DocflowObservationStatus = 'started' | 'completed' | 'failed';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A single upsert of one node invocation's state. The gateway upserts on
|
|
22
|
+
* (run_id, attempt, node_id, invocation_seq); a later `completed`/`failed`
|
|
23
|
+
* write for the same key supersedes an earlier `started` (status precedence
|
|
24
|
+
* started < completed/failed, never regressing to `started`).
|
|
25
|
+
*/
|
|
26
|
+
export type DocflowObservationUpsert = {
|
|
27
|
+
runId: string;
|
|
28
|
+
attempt: number;
|
|
29
|
+
nodeId: string;
|
|
30
|
+
invocationSeq: number;
|
|
31
|
+
invocationId?: string | null;
|
|
32
|
+
status: DocflowObservationStatus;
|
|
33
|
+
inputs?: PlayDocflowNodeIoPreviewMap | null;
|
|
34
|
+
outputs?: PlayDocflowNodeIoPreviewMap | null;
|
|
35
|
+
inputsTruncated?: boolean;
|
|
36
|
+
outputsTruncated?: boolean;
|
|
37
|
+
error?: string | null;
|
|
38
|
+
/** Epoch ms; started writes carry it, terminal writes carry a settled ts. */
|
|
39
|
+
at: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** A durable observation row read back for terminal ledger reconciliation. */
|
|
43
|
+
export type DocflowObservationRecord = {
|
|
44
|
+
runId: string;
|
|
45
|
+
attempt: number;
|
|
46
|
+
nodeId: string;
|
|
47
|
+
invocationSeq: number;
|
|
48
|
+
invocationId: string | null;
|
|
49
|
+
status: DocflowObservationStatus;
|
|
50
|
+
inputs: PlayDocflowNodeIoPreviewMap | null;
|
|
51
|
+
outputs: PlayDocflowNodeIoPreviewMap | null;
|
|
52
|
+
inputsTruncated: boolean;
|
|
53
|
+
outputsTruncated: boolean;
|
|
54
|
+
error: string | null;
|
|
55
|
+
startedAt: number | null;
|
|
56
|
+
settledAt: number | null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Numeric precedence so a late `started` never clobbers a settled write. */
|
|
60
|
+
export function docflowObservationStatusRank(
|
|
61
|
+
status: DocflowObservationStatus,
|
|
62
|
+
): number {
|
|
63
|
+
return status === 'started' ? 0 : 1;
|
|
64
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const DYNAMIC_PLAY_WORKER_ARTIFACT_VERSION =
|
|
2
|
-
'
|
|
2
|
+
'h26-docflow-node-io-runtime';
|