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.
Files changed (44) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +17 -1
  2. package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +1 -1
  3. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  4. package/dist/bundling-sources/sdk/src/types.ts +43 -1
  5. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +30 -1
  6. package/dist/bundling-sources/shared_libs/play-runtime/cell-provenance.ts +231 -0
  7. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1094 -128
  8. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +178 -9
  9. package/dist/bundling-sources/shared_libs/play-runtime/docflow-node-io.ts +634 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/docflow-observation.ts +64 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/dynamic-worker-version.ts +1 -1
  12. package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +18 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/live-state-contract.ts +33 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/log-provenance.ts +251 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/play-node-scope.ts +160 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +6 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +27 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +43 -5
  19. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +12 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +26 -4
  21. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +6 -1
  22. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +83 -0
  23. package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +3 -0
  24. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +49 -1
  25. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +375 -29
  26. package/dist/bundling-sources/shared_libs/plays/docflow-binding-owner.ts +636 -0
  27. package/dist/bundling-sources/shared_libs/plays/docflow-binding.ts +598 -0
  28. package/dist/bundling-sources/shared_libs/plays/docflow.ts +1645 -0
  29. package/dist/bundling-sources/shared_libs/plays/play-exports.ts +202 -0
  30. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +16 -1
  31. package/dist/bundling-sources/shared_libs/plays/ts-ast.ts +48 -0
  32. package/dist/cli/index.js +994 -312
  33. package/dist/cli/index.mjs +994 -312
  34. package/dist/{compiler-manifest-Cj3--4ZJ.d.mts → compiler-manifest-Bl8kmLx9.d.mts} +118 -0
  35. package/dist/{compiler-manifest-Cj3--4ZJ.d.ts → compiler-manifest-Bl8kmLx9.d.ts} +118 -0
  36. package/dist/index.d.mts +47 -2
  37. package/dist/index.d.ts +47 -2
  38. package/dist/index.js +419 -59
  39. package/dist/index.mjs +419 -59
  40. package/dist/install-integrity.json +12 -2
  41. package/dist/plays/bundle-play-file.d.mts +2 -2
  42. package/dist/plays/bundle-play-file.d.ts +2 -2
  43. package/dist/plays/bundle-play-file.mjs +1361 -45
  44. 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
- 'h24-map-dispatch-batch-metadata';
2
+ 'h26-docflow-node-io-runtime';