deepline 0.2.55 → 0.2.57
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 +14 -0
- package/dist/bundling-sources/sdk/src/http.ts +19 -1
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/stream-reconnect.ts +6 -0
- package/dist/bundling-sources/sdk/src/types.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +146 -12
- package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +6 -3
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1631 -748
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +20 -0
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +81 -52
- package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +146 -6
- package/dist/bundling-sources/shared_libs/play-runtime/gateway-auth-session.ts +56 -22
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +14 -11
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +21 -2
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +81 -21
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +27 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +49 -10
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +96 -3
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +341 -67
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +10 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +8 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-pg-driver-pg.ts +17 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-pg-driver.ts +4 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-store-adapter.ts +17 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-writer.ts +16 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-writer.ts +25 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +80 -1
- package/dist/bundling-sources/shared_libs/plays/docflow.ts +113 -14
- package/dist/bundling-sources/shared_libs/plays/play-exports.ts +53 -4
- package/dist/bundling-sources/shared_libs/security/safe-fetch.ts +9 -0
- package/dist/bundling-sources/shared_libs/security/safe-outbound-fetch.ts +85 -48
- package/dist/cli/index.js +429 -54
- package/dist/cli/index.mjs +409 -28
- package/dist/{compiler-manifest-Bl8kmLx9.d.mts → compiler-manifest-TgaC4DeD.d.mts} +13 -0
- package/dist/{compiler-manifest-Bl8kmLx9.d.ts → compiler-manifest-TgaC4DeD.d.ts} +13 -0
- package/dist/index.d.mts +21 -3
- package/dist/index.d.ts +21 -3
- package/dist/index.js +29 -2
- package/dist/index.mjs +29 -2
- package/dist/install-integrity.json +2 -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 +78 -18
- package/package.json +1 -1
|
@@ -11,6 +11,20 @@ import {
|
|
|
11
11
|
} from './runtime-pg-driver';
|
|
12
12
|
|
|
13
13
|
function wrapPgClient(client: PoolClient): RuntimePoolClient {
|
|
14
|
+
let released = false;
|
|
15
|
+
const releaseClient = (destroy = false) => {
|
|
16
|
+
if (released) return;
|
|
17
|
+
released = true;
|
|
18
|
+
client.release(destroy);
|
|
19
|
+
};
|
|
20
|
+
const destroyTransport = () => {
|
|
21
|
+
// `release(true)` is node-postgres' supported pool-client destruction
|
|
22
|
+
// path. The pool removes the client and Client.end() destroys the socket
|
|
23
|
+
// when a query is active, while keeping pool accounting correct. Runtime
|
|
24
|
+
// callers still run their ordinary `finally { release() }` path after a
|
|
25
|
+
// deadline destroys the transport, so this wrapper owns idempotence.
|
|
26
|
+
releaseClient(true);
|
|
27
|
+
};
|
|
14
28
|
return {
|
|
15
29
|
query: <R extends Record<string, unknown> = Record<string, unknown>>(
|
|
16
30
|
text: string,
|
|
@@ -24,9 +38,10 @@ function wrapPgClient(client: PoolClient): RuntimePoolClient {
|
|
|
24
38
|
)(text, params).then((result) => ({
|
|
25
39
|
rows: result.rows,
|
|
26
40
|
})),
|
|
27
|
-
release: () => {
|
|
28
|
-
|
|
41
|
+
release: (destroy = false) => {
|
|
42
|
+
releaseClient(destroy);
|
|
29
43
|
},
|
|
44
|
+
destroy: destroyTransport,
|
|
30
45
|
};
|
|
31
46
|
}
|
|
32
47
|
|
|
@@ -13,7 +13,10 @@ export interface RuntimePoolClient {
|
|
|
13
13
|
text: string,
|
|
14
14
|
params?: unknown[],
|
|
15
15
|
): Promise<{ rows: R[] }>;
|
|
16
|
-
|
|
16
|
+
/** Destroy the physical connection when an active query exceeded its bound. */
|
|
17
|
+
release(destroy?: boolean): void;
|
|
18
|
+
/** Interrupt an active query by destroying the physical transport. */
|
|
19
|
+
destroy?(error?: Error): void;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export interface RuntimePool {
|
|
@@ -9,7 +9,10 @@ import type {
|
|
|
9
9
|
RuntimeStepReceipt,
|
|
10
10
|
SkipRuntimeStepReceiptInput,
|
|
11
11
|
} from './ctx-types';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
RUNTIME_RECEIPT_GATEWAY_BATCH_MAX_BYTES,
|
|
14
|
+
RUNTIME_RECEIPT_WRITER_TARGET_BATCH_BYTES,
|
|
15
|
+
} from './output-size-limits';
|
|
13
16
|
import {
|
|
14
17
|
acquireRuntimeReceiptExecutionLockViaAppRuntime,
|
|
15
18
|
AppRuntimeApiResponseError,
|
|
@@ -106,6 +109,12 @@ export type RuntimeReceiptStoreHandlers = Required<
|
|
|
106
109
|
Pick<ContextOptions, ReceiptHandlerName>
|
|
107
110
|
>;
|
|
108
111
|
|
|
112
|
+
// Four ambiguous 30-second receipt requests previously consumed the writer's
|
|
113
|
+
// entire two-minute budget. Receipt operations are idempotent and buffered, so
|
|
114
|
+
// keep retry ownership here and allow independent transports enough attempts
|
|
115
|
+
// to recover from a short gateway/network impairment.
|
|
116
|
+
export const RUNTIME_RECEIPT_STORE_RETRY_BUDGET_MS = 5 * 60_000;
|
|
117
|
+
|
|
109
118
|
/**
|
|
110
119
|
* The runner's single Work Receipt Adapter.
|
|
111
120
|
*
|
|
@@ -116,6 +125,7 @@ export type RuntimeReceiptStoreHandlers = Required<
|
|
|
116
125
|
*/
|
|
117
126
|
export class RuntimeReceiptStoreAdapter {
|
|
118
127
|
readonly handlers: RuntimeReceiptStoreHandlers;
|
|
128
|
+
readonly claimsEstablishExecutionFence = true;
|
|
119
129
|
readonly #writer: RuntimeReceiptWriter<ReceiptCommand, ReceiptOutput>;
|
|
120
130
|
#serial = 0;
|
|
121
131
|
|
|
@@ -125,6 +135,7 @@ export class RuntimeReceiptStoreAdapter {
|
|
|
125
135
|
runId: string;
|
|
126
136
|
maxBatchSize?: number;
|
|
127
137
|
maxBatchBytes?: number;
|
|
138
|
+
targetBatchBytes?: number;
|
|
128
139
|
maxFlushMs?: number;
|
|
129
140
|
maxBufferedBytes?: number;
|
|
130
141
|
onRetryTelemetry?: (line: string) => void;
|
|
@@ -138,8 +149,11 @@ export class RuntimeReceiptStoreAdapter {
|
|
|
138
149
|
maxBatchSize: options.maxBatchSize,
|
|
139
150
|
maxBatchBytes:
|
|
140
151
|
options.maxBatchBytes ?? RUNTIME_RECEIPT_GATEWAY_BATCH_MAX_BYTES,
|
|
152
|
+
targetBatchBytes:
|
|
153
|
+
options.targetBatchBytes ?? RUNTIME_RECEIPT_WRITER_TARGET_BATCH_BYTES,
|
|
141
154
|
maxFlushMs: options.maxFlushMs,
|
|
142
155
|
maxBufferedBytes: options.maxBufferedBytes,
|
|
156
|
+
maxRetryElapsedMs: RUNTIME_RECEIPT_STORE_RETRY_BUDGET_MS,
|
|
143
157
|
classifyRetryableError: classifyReceiptRetry,
|
|
144
158
|
onRetryEvent: (event) =>
|
|
145
159
|
emitReceiptWriterRetryTelemetry({
|
|
@@ -309,6 +323,7 @@ function receiptRetryErrorTelemetry(error: unknown): Record<string, unknown> {
|
|
|
309
323
|
errorName: error.name,
|
|
310
324
|
transportAttempts: error.attempts,
|
|
311
325
|
transportAction: error.action,
|
|
326
|
+
transportAttemptId: error.transportAttemptId,
|
|
312
327
|
};
|
|
313
328
|
}
|
|
314
329
|
if (error instanceof AppRuntimeApiResponseError) {
|
|
@@ -343,6 +358,7 @@ function emitReceiptWriterRetryTelemetry(input: {
|
|
|
343
358
|
action: receiptCommandTelemetryAction(event.firstInput),
|
|
344
359
|
batchId: event.batchId,
|
|
345
360
|
batchSize: event.batchSize,
|
|
361
|
+
batchBytes: event.batchBytes,
|
|
346
362
|
attempt: event.attempt,
|
|
347
363
|
elapsedMs: event.elapsedMs,
|
|
348
364
|
retryAfterMs: event.retryAfterMs,
|
|
@@ -27,6 +27,7 @@ export type RuntimeReceiptWriterRetryEvent<Input> = {
|
|
|
27
27
|
attempt: number;
|
|
28
28
|
batchId: number;
|
|
29
29
|
batchSize: number;
|
|
30
|
+
batchBytes: number;
|
|
30
31
|
firstInput: Input;
|
|
31
32
|
elapsedMs: number;
|
|
32
33
|
retryAfterMs: number;
|
|
@@ -103,6 +104,7 @@ export class RuntimeReceiptWriter<Input, Output> {
|
|
|
103
104
|
readonly #estimateBytes: (input: Input) => number;
|
|
104
105
|
readonly #maxBatchSize: number;
|
|
105
106
|
readonly #maxBatchBytes: number;
|
|
107
|
+
readonly #targetBatchBytes: number;
|
|
106
108
|
readonly #maxBufferedBytes: number;
|
|
107
109
|
readonly #maxFlushMs: number;
|
|
108
110
|
readonly #maxRetryElapsedMs: number;
|
|
@@ -135,6 +137,7 @@ export class RuntimeReceiptWriter<Input, Output> {
|
|
|
135
137
|
estimateBytes?: (input: Input) => number;
|
|
136
138
|
maxBatchSize?: number;
|
|
137
139
|
maxBatchBytes?: number;
|
|
140
|
+
targetBatchBytes?: number;
|
|
138
141
|
maxBufferedBytes?: number;
|
|
139
142
|
maxFlushMs?: number;
|
|
140
143
|
maxRetryElapsedMs?: number;
|
|
@@ -154,6 +157,10 @@ export class RuntimeReceiptWriter<Input, Output> {
|
|
|
154
157
|
normalizePositiveInteger(options.maxBatchBytes, this.#maxBufferedBytes),
|
|
155
158
|
this.#maxBufferedBytes,
|
|
156
159
|
);
|
|
160
|
+
this.#targetBatchBytes = Math.min(
|
|
161
|
+
normalizePositiveInteger(options.targetBatchBytes, this.#maxBatchBytes),
|
|
162
|
+
this.#maxBatchBytes,
|
|
163
|
+
);
|
|
157
164
|
this.#maxFlushMs = Math.max(0, Math.floor(options.maxFlushMs ?? 5));
|
|
158
165
|
this.#maxRetryElapsedMs = normalizePositiveInteger(
|
|
159
166
|
options.maxRetryElapsedMs,
|
|
@@ -251,7 +258,7 @@ export class RuntimeReceiptWriter<Input, Output> {
|
|
|
251
258
|
this.#bufferedBytes += pending.bytes;
|
|
252
259
|
if (
|
|
253
260
|
this.#queued.length >= this.#maxBatchSize ||
|
|
254
|
-
this.#queuedBytesAtHead() >= this.#
|
|
261
|
+
this.#queuedBytesAtHead() >= this.#targetBatchBytes
|
|
255
262
|
) {
|
|
256
263
|
this.#clearFlushTimer();
|
|
257
264
|
this.#startPump();
|
|
@@ -334,7 +341,10 @@ export class RuntimeReceiptWriter<Input, Output> {
|
|
|
334
341
|
while (batch.length < this.#maxBatchSize) {
|
|
335
342
|
const next = this.#queued[0];
|
|
336
343
|
if (!next || !Object.is(this.#batchKey(next.input), key)) break;
|
|
337
|
-
if (
|
|
344
|
+
if (
|
|
345
|
+
batch.length > 0 &&
|
|
346
|
+
batchBytes + next.bytes > this.#targetBatchBytes
|
|
347
|
+
) {
|
|
338
348
|
break;
|
|
339
349
|
}
|
|
340
350
|
this.#queued.shift();
|
|
@@ -427,6 +437,10 @@ export class RuntimeReceiptWriter<Input, Output> {
|
|
|
427
437
|
attempt: input.attempt,
|
|
428
438
|
batchId: input.batchId,
|
|
429
439
|
batchSize: input.batch.length,
|
|
440
|
+
batchBytes: input.batch.reduce(
|
|
441
|
+
(total, pending) => total + pending.bytes,
|
|
442
|
+
0,
|
|
443
|
+
),
|
|
430
444
|
firstInput,
|
|
431
445
|
elapsedMs: Date.now() - input.startedAt,
|
|
432
446
|
retryAfterMs: input.retryAfterMs,
|
|
@@ -70,6 +70,14 @@ export type RuntimeSheetRowWriterSummary = {
|
|
|
70
70
|
terminalRows: number;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
export type RuntimeSheetRowWriterDiagnostics = {
|
|
74
|
+
activeKind: 'terminal' | 'checkpoint' | null;
|
|
75
|
+
activeRows: number;
|
|
76
|
+
activeStartedAt: number | null;
|
|
77
|
+
blockedTerminalRows: number;
|
|
78
|
+
queuedTerminalRows: number;
|
|
79
|
+
};
|
|
80
|
+
|
|
73
81
|
export type RuntimeSheetRowSettlement = {
|
|
74
82
|
/**
|
|
75
83
|
* Resolves once the bounded live buffer owns this row. Awaiting this promise
|
|
@@ -159,6 +167,7 @@ export class RuntimeSheetRowWriter<Row, Update> {
|
|
|
159
167
|
readonly #controller = new AbortController();
|
|
160
168
|
|
|
161
169
|
#active: PendingWrite<Row, Update>[] | null = null;
|
|
170
|
+
#activeStartedAt: number | null = null;
|
|
162
171
|
#bufferedBytes = 0;
|
|
163
172
|
#liveBytes = 0;
|
|
164
173
|
#timer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -264,6 +273,20 @@ export class RuntimeSheetRowWriter<Row, Update> {
|
|
|
264
273
|
return this.#finishPromise;
|
|
265
274
|
}
|
|
266
275
|
|
|
276
|
+
diagnostics(): RuntimeSheetRowWriterDiagnostics {
|
|
277
|
+
return {
|
|
278
|
+
activeKind: this.#active?.[0]?.kind ?? null,
|
|
279
|
+
activeRows: this.#active?.length ?? 0,
|
|
280
|
+
activeStartedAt: this.#activeStartedAt,
|
|
281
|
+
blockedTerminalRows: this.#blocked.filter(
|
|
282
|
+
(entry) => entry.kind === 'terminal',
|
|
283
|
+
).length,
|
|
284
|
+
queuedTerminalRows: [...this.#live.values()].filter(
|
|
285
|
+
(entry) => entry.kind === 'terminal',
|
|
286
|
+
).length,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
267
290
|
#enqueueCheckpointUpdate(update: Update): Promise<void> {
|
|
268
291
|
const key = normalizedKey(this.#checkpointKey(update));
|
|
269
292
|
if (this.#terminalAcceptedKeys.has(key)) return Promise.resolve();
|
|
@@ -403,6 +426,7 @@ export class RuntimeSheetRowWriter<Row, Update> {
|
|
|
403
426
|
if (batch.length === 0) return;
|
|
404
427
|
const batchKind = batch[0]!.kind;
|
|
405
428
|
this.#active = batch;
|
|
429
|
+
this.#activeStartedAt = Date.now();
|
|
406
430
|
try {
|
|
407
431
|
const result = await this.#writeIdenticalBatchWithRetry(batch);
|
|
408
432
|
const resultCommitted = result ? result.committed : undefined;
|
|
@@ -429,6 +453,7 @@ export class RuntimeSheetRowWriter<Row, Update> {
|
|
|
429
453
|
);
|
|
430
454
|
this.#bufferedBytes = Math.max(0, this.#bufferedBytes - releasedBytes);
|
|
431
455
|
this.#active = null;
|
|
456
|
+
this.#activeStartedAt = null;
|
|
432
457
|
this.#admitBlocked();
|
|
433
458
|
this.#notifyStateChanged();
|
|
434
459
|
}
|
|
@@ -58,6 +58,9 @@ export type PlaySchedulerSubmitInput = {
|
|
|
58
58
|
force?: boolean;
|
|
59
59
|
/** Explicit cache bypass for completed ctx.tools.execute receipts. */
|
|
60
60
|
forceToolRefresh?: boolean | null;
|
|
61
|
+
/** Validated per-run ceiling for provider-tool executions and ctx.fetch. */
|
|
62
|
+
maxConcurrentExternalCalls?: number | null;
|
|
63
|
+
maxConcurrentRows?: number | null;
|
|
61
64
|
inputFile?: {
|
|
62
65
|
name?: string;
|
|
63
66
|
path?: string;
|
|
@@ -56,6 +56,8 @@ export type PlayExecutionSuspension =
|
|
|
56
56
|
exitCodePath: string;
|
|
57
57
|
/** Exact customer-code completion fence inside the Daytona sandbox. */
|
|
58
58
|
runtimeCompletedPath?: string;
|
|
59
|
+
/** Bounded watchdog-owned cause marker for crash-only diagnostics. */
|
|
60
|
+
terminationDiagnosticPath?: string;
|
|
59
61
|
startedAtMs: number;
|
|
60
62
|
/** Scheduler-owned liveness deadline. The sandbox can renew it only by
|
|
61
63
|
* persisting `heartbeat_at` through the receipt gateway. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getRuntimeEnv } from '../runtime-env';
|
|
2
|
+
import type { PlayRunnerEvent } from './protocol';
|
|
2
3
|
|
|
3
4
|
export const PLAY_RUNTIME_TEST_FAULT_HEADER = 'x-deepline-test-fault';
|
|
4
5
|
|
|
@@ -13,7 +14,23 @@ export type RuntimeTestFaultName =
|
|
|
13
14
|
* deterministic real-Pg-pool pressure window without changing an action's
|
|
14
15
|
* SQL or outcome.
|
|
15
16
|
*/
|
|
16
|
-
| 'receipt_gateway_hold_ms'
|
|
17
|
+
| 'receipt_gateway_hold_ms'
|
|
18
|
+
/**
|
|
19
|
+
* Synthetic-only bulk-claim fault. The runner forwards this token on the
|
|
20
|
+
* first qualifying physical claim request only. The gateway holds the real
|
|
21
|
+
* checked-out scheduler client for the supplied milliseconds, allowing the
|
|
22
|
+
* claim-operation deadline and connection destruction path to be exercised
|
|
23
|
+
* end to end without making the fault Machine-local.
|
|
24
|
+
*/
|
|
25
|
+
| 'receipt_claim_query_hold_once_ms'
|
|
26
|
+
/**
|
|
27
|
+
* Synthetic-only response-boundary fault. The runner fully receives a
|
|
28
|
+
* successful bulk receipt-claim response, then reports the configured first
|
|
29
|
+
* N physical requests as request timeouts. This proves ambiguous-delivery
|
|
30
|
+
* replay without depending on which gateway Machine accepted the request.
|
|
31
|
+
*/
|
|
32
|
+
| 'receipt_claim_response_timeout'
|
|
33
|
+
| 'runtime_sheet_page_tail_hold_ms';
|
|
17
34
|
|
|
18
35
|
export type RuntimeTestFaultRegistry = {
|
|
19
36
|
consume(name: RuntimeTestFaultName): boolean;
|
|
@@ -75,6 +92,9 @@ const SUPPORTED_RUNTIME_TEST_FAULTS = new Set<RuntimeTestFaultName>([
|
|
|
75
92
|
'worker_receipt_complete_write_fail',
|
|
76
93
|
'invocation_response_delivery_abort',
|
|
77
94
|
'receipt_gateway_hold_ms',
|
|
95
|
+
'receipt_claim_query_hold_once_ms',
|
|
96
|
+
'receipt_claim_response_timeout',
|
|
97
|
+
'runtime_sheet_page_tail_hold_ms',
|
|
78
98
|
]);
|
|
79
99
|
|
|
80
100
|
const RUNTIME_TEST_POLICY_MS_FIELDS = new Set([
|
|
@@ -149,6 +169,65 @@ function parseRuntimeTestFaultHeader(
|
|
|
149
169
|
return faults;
|
|
150
170
|
}
|
|
151
171
|
|
|
172
|
+
/** Parse a known fault count without authorizing a request. Unknown/skewed headers return 0. */
|
|
173
|
+
export function recognizedRuntimeTestFaultCount(
|
|
174
|
+
rawHeader: string | null | undefined,
|
|
175
|
+
name: RuntimeTestFaultName,
|
|
176
|
+
): number {
|
|
177
|
+
const header = rawHeader?.trim();
|
|
178
|
+
if (!header) return 0;
|
|
179
|
+
const parsed = parseRuntimeTestFaultHeader(header);
|
|
180
|
+
if ('error' in parsed) return 0;
|
|
181
|
+
return parsed.get(name) ?? 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function runtimeSheetPageTailRowsContainTarget(
|
|
185
|
+
rows: readonly unknown[],
|
|
186
|
+
): boolean {
|
|
187
|
+
return rows.some((row) => {
|
|
188
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) return false;
|
|
189
|
+
const record = row as Record<string, unknown>;
|
|
190
|
+
return record.inputIndex === 999 || record.input_index === 999;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function runtimeSheetPageTailWriteMarkerEvent(
|
|
195
|
+
runtimeTestFaultHeader: string | null | undefined,
|
|
196
|
+
rows: readonly Record<string, unknown>[],
|
|
197
|
+
tableNamespace: string,
|
|
198
|
+
dbSessionStrategy: string | null | undefined,
|
|
199
|
+
): PlayRunnerEvent | null {
|
|
200
|
+
if (
|
|
201
|
+
dbSessionStrategy !== 'gateway_only' ||
|
|
202
|
+
recognizedRuntimeTestFaultCount(
|
|
203
|
+
runtimeTestFaultHeader,
|
|
204
|
+
'runtime_sheet_page_tail_hold_ms',
|
|
205
|
+
) <= 0 ||
|
|
206
|
+
!runtimeSheetPageTailRowsContainTarget(rows)
|
|
207
|
+
) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
type: 'log',
|
|
212
|
+
at: new Date().toISOString(),
|
|
213
|
+
source: 'play',
|
|
214
|
+
line: `[runtime.sheet-page-tail-write] phase=start table=${tableNamespace} rows=${rows.length}`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function runtimeSheetPageTailHoldCompletedEvent(
|
|
219
|
+
holdMs: number | null | undefined,
|
|
220
|
+
tableNamespace: string,
|
|
221
|
+
): PlayRunnerEvent | null {
|
|
222
|
+
if (!Number.isInteger(holdMs) || (holdMs ?? 0) <= 0) return null;
|
|
223
|
+
return {
|
|
224
|
+
type: 'log',
|
|
225
|
+
at: new Date().toISOString(),
|
|
226
|
+
source: 'play',
|
|
227
|
+
line: `[runtime.sheet-page-tail-hold] phase=finish table=${tableNamespace} hold_ms=${holdMs}`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
152
231
|
function readPositiveIntegerField(input: {
|
|
153
232
|
value: unknown;
|
|
154
233
|
path: string;
|
|
@@ -38,6 +38,19 @@ export type PlayDocflowNode = {
|
|
|
38
38
|
id: string;
|
|
39
39
|
label: string;
|
|
40
40
|
kind: PlayDocflowNodeKind;
|
|
41
|
+
/**
|
|
42
|
+
* No statement in this play runs this box — the author said so with
|
|
43
|
+
* `class <id> sketch`. See {@link SKETCH_CLASS}.
|
|
44
|
+
*
|
|
45
|
+
* Deliberately NOT a `kind`. What a box IS (an action, a decision, a dataset)
|
|
46
|
+
* and whether code binds it are two different questions, and folding the
|
|
47
|
+
* second into the first was wrong in a way the tests caught immediately: a
|
|
48
|
+
* sketched diamond stopped being a decision, so it lost its shape on the
|
|
49
|
+
* canvas and the branch-label lint stopped checking its arms. Absent rather
|
|
50
|
+
* than `false` when bound, so the JSON a bound diagram hashes to is byte for
|
|
51
|
+
* byte what it was before sketches existed.
|
|
52
|
+
*/
|
|
53
|
+
sketch?: true;
|
|
41
54
|
};
|
|
42
55
|
|
|
43
56
|
/**
|
|
@@ -225,6 +238,23 @@ const ATTRIBUTE = /([A-Za-z][\w-]*)\s*:\s*"([^"\n]*)"/y;
|
|
|
225
238
|
const MERMAID_NODE_ATTRIBUTES = ['label', 'type', 'in', 'out', 'arm'] as const;
|
|
226
239
|
const LEGACY_DOCFLOW_ATTRIBUTES = ['id', ...MERMAID_NODE_ATTRIBUTES] as const;
|
|
227
240
|
const MERMAID_NODE_ID = /^[A-Za-z][\w-]*/;
|
|
241
|
+
/**
|
|
242
|
+
* `class a,b,c sketch` — the author declaring that no statement runs these boxes.
|
|
243
|
+
*
|
|
244
|
+
* Every other box in a diagram must point at a statement in this play's source,
|
|
245
|
+
* because that is what makes the diagram a claim about the code rather than a
|
|
246
|
+
* picture of it. Some boxes honestly cannot: a cascade whose legs live in a
|
|
247
|
+
* sibling module, a loop over a provider list, the outcome boxes hanging off a
|
|
248
|
+
* decision. Those boxes still belong on the canvas — they name the real route —
|
|
249
|
+
* and this is how the author says so out loud, so the reader is told "sketch"
|
|
250
|
+
* instead of being shown a box that looks misconfigured.
|
|
251
|
+
*
|
|
252
|
+
* It is mermaid's own `class` statement rather than a Deepline directive, so the
|
|
253
|
+
* block stays a diagram any mermaid renderer can draw. Class names other than
|
|
254
|
+
* `sketch` remain styling this dashboard does not apply, exactly as before.
|
|
255
|
+
*/
|
|
256
|
+
const SKETCH_CLASS =
|
|
257
|
+
/^\s*class\s+([A-Za-z][\w-]*(?:\s*,\s*[A-Za-z][\w-]*)*)\s+sketch\s*;?\s*$/;
|
|
228
258
|
/**
|
|
229
259
|
* Mermaid's node shapes, longest opener FIRST.
|
|
230
260
|
*
|
|
@@ -601,7 +631,14 @@ function levenshtein(left: string, right: string): number {
|
|
|
601
631
|
|
|
602
632
|
const MERMAID_BLOCK = /\/\*\*\s*@mermaid(?:\s|\r?\n)([\s\S]*?)\*\//g;
|
|
603
633
|
const LEGACY_BLOCK = /\/\*\*\s*@docflow(?:\s|\r?\n)([\s\S]*?)\*\//;
|
|
604
|
-
|
|
634
|
+
/**
|
|
635
|
+
* What a block header may name: an export name, or the play's own kebab-case
|
|
636
|
+
* name. Hyphens are in the set for the second — `@mermaid name-to-linkedin-url-
|
|
637
|
+
* waterfall` is the header worth writing, and while it was identifier-only the
|
|
638
|
+
* whole line silently became the diagram's first line, which surfaced as
|
|
639
|
+
* "Docflow must start with `flowchart`" and named nothing.
|
|
640
|
+
*/
|
|
641
|
+
const BLOCK_EXPORT_HEADER = /^[A-Za-z_$][\w$-]*$/;
|
|
605
642
|
|
|
606
643
|
/** Strips the JSDoc `*` gutter and trims, leaving the authored block text. */
|
|
607
644
|
function cleanBlockBody(raw: string): string {
|
|
@@ -613,7 +650,8 @@ function cleanBlockBody(raw: string): string {
|
|
|
613
650
|
}
|
|
614
651
|
|
|
615
652
|
/**
|
|
616
|
-
* Splits `/** @mermaid
|
|
653
|
+
* Splits `/** @mermaid contact-to-phone-waterfall` into the play it names and
|
|
654
|
+
* the diagram itself.
|
|
617
655
|
*
|
|
618
656
|
* The name is the first token after the tag, the same place `// @mermaid-node
|
|
619
657
|
* <id>` puts its target, so one rule covers both halves of the grammar. It is
|
|
@@ -649,6 +687,8 @@ type ParsedDocflowBlockGraph = {
|
|
|
649
687
|
edges: PlayDocflowEdge[];
|
|
650
688
|
subgraphs: Map<string, PlayDocflowSubgraph>;
|
|
651
689
|
ignoredDirectives: string[];
|
|
690
|
+
/** Ids a `class … sketch` line declared to run no statement. */
|
|
691
|
+
sketchIds: Set<string>;
|
|
652
692
|
};
|
|
653
693
|
|
|
654
694
|
type ParsedDocflowBlock = ParsedDocflowBlockGraph & {
|
|
@@ -701,6 +741,9 @@ function parseDocflowBlockGraph(
|
|
|
701
741
|
// an edge on an earlier line may reference a subgraph declared later.
|
|
702
742
|
const subgraphs = new Map<string, PlayDocflowSubgraph>();
|
|
703
743
|
const ignoredDirectives: string[] = [];
|
|
744
|
+
// Boxes the author declared to be a sketch, via mermaid's own
|
|
745
|
+
// `class a,b sketch`. See {@link SKETCH_CLASS}.
|
|
746
|
+
const sketchIds = new Set<string>();
|
|
704
747
|
const subgraphStack: PlayDocflowSubgraph[] = [];
|
|
705
748
|
const subgraphIds = new Set<string>();
|
|
706
749
|
if (syntax === 'mermaid') {
|
|
@@ -796,7 +839,15 @@ function parseDocflowBlockGraph(
|
|
|
796
839
|
const isDirective =
|
|
797
840
|
/^\s*(?:direction|classDef|class|style|linkStyle|click)\b/i.test(line);
|
|
798
841
|
if (syntax === 'mermaid' && isDirective) {
|
|
799
|
-
|
|
842
|
+
const sketch = SKETCH_CLASS.exec(line);
|
|
843
|
+
if (sketch) {
|
|
844
|
+
for (const id of sketch[1]!.split(',')) {
|
|
845
|
+
const trimmed = id.trim();
|
|
846
|
+
if (trimmed) sketchIds.add(trimmed);
|
|
847
|
+
}
|
|
848
|
+
} else {
|
|
849
|
+
ignoredDirectives.push(line.trim());
|
|
850
|
+
}
|
|
800
851
|
}
|
|
801
852
|
if (syntax === 'mermaid' && !isDirective) {
|
|
802
853
|
declaredOnThisLine = addNodes(line, line);
|
|
@@ -859,7 +910,20 @@ function parseDocflowBlockGraph(
|
|
|
859
910
|
);
|
|
860
911
|
}
|
|
861
912
|
|
|
862
|
-
|
|
913
|
+
for (const id of sketchIds) {
|
|
914
|
+
const node = nodes.get(id);
|
|
915
|
+
if (!node) {
|
|
916
|
+
errors.push(
|
|
917
|
+
subgraphs.has(id)
|
|
918
|
+
? `Docflow \`class ${id} sketch\` names a subgraph. A subgraph is a region, not a box, and never binds code — drop it from the class line.`
|
|
919
|
+
: `Docflow \`class ${id} sketch\` names "${id}", which this diagram does not draw.`,
|
|
920
|
+
);
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
node.sketch = true;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return { direction, nodes, edges, subgraphs, ignoredDirectives, sketchIds };
|
|
863
927
|
}
|
|
864
928
|
|
|
865
929
|
function materializeDocflow(block: ParsedDocflowBlock): PlayDocflow {
|
|
@@ -889,7 +953,11 @@ export function parsePlayDocflowFile(
|
|
|
889
953
|
sourceCode: string,
|
|
890
954
|
): PlayDocflowFileParseResult {
|
|
891
955
|
const errors: string[] = [];
|
|
892
|
-
|
|
956
|
+
// Ids whose annotation was written but refused. They are NOT unbound boxes:
|
|
957
|
+
// the author already has one actionable error about them, and telling them to
|
|
958
|
+
// "bind it" on the next line is telling them to do what they just did.
|
|
959
|
+
const rejectedNodeIds = new Set<string>();
|
|
960
|
+
const bindings = parseDocflowBindings(sourceCode, errors, rejectedNodeIds);
|
|
893
961
|
|
|
894
962
|
const rawBlocks: Array<{
|
|
895
963
|
syntax: NonNullable<PlayDocflow['syntax']>;
|
|
@@ -923,7 +991,13 @@ export function parsePlayDocflowFile(
|
|
|
923
991
|
}
|
|
924
992
|
|
|
925
993
|
resolveBlockExports(sourceCode, parsedBlocks, errors);
|
|
926
|
-
attachBindingsToBlocks(
|
|
994
|
+
attachBindingsToBlocks(
|
|
995
|
+
sourceCode,
|
|
996
|
+
parsedBlocks,
|
|
997
|
+
bindings,
|
|
998
|
+
errors,
|
|
999
|
+
rejectedNodeIds,
|
|
1000
|
+
);
|
|
927
1001
|
|
|
928
1002
|
return {
|
|
929
1003
|
blocks: parsedBlocks
|
|
@@ -1163,7 +1237,7 @@ function projectRecordedArms(
|
|
|
1163
1237
|
// One decision, one meaning per token. Two arms both claiming `run` is the
|
|
1164
1238
|
// exact defect the recorded identity exists to make impossible, so it is
|
|
1165
1239
|
// refused at the source rather than resolved by precedence downstream.
|
|
1166
|
-
const key = `${edge.from}
|
|
1240
|
+
const key = `${edge.from}\u0000${binding.arm}`;
|
|
1167
1241
|
const already = declaredBy.get(key);
|
|
1168
1242
|
if (already !== undefined) {
|
|
1169
1243
|
errors.push(
|
|
@@ -1181,6 +1255,7 @@ function attachBindingsToBlocks(
|
|
|
1181
1255
|
blocks: ParsedDocflowBlock[],
|
|
1182
1256
|
bindings: readonly PlayDocflowBinding[],
|
|
1183
1257
|
errors: string[],
|
|
1258
|
+
rejectedNodeIds: ReadonlySet<string>,
|
|
1184
1259
|
): void {
|
|
1185
1260
|
const lineStarts = sourceLineStartsForDocflow(sourceCode);
|
|
1186
1261
|
const exportRanges = playExportSourceRanges(sourceCode);
|
|
@@ -1227,17 +1302,33 @@ function attachBindingsToBlocks(
|
|
|
1227
1302
|
for (const binding of block.bindings) {
|
|
1228
1303
|
const node = block.nodes.get(binding.nodeId)!;
|
|
1229
1304
|
if (binding.label) node.label = binding.label;
|
|
1305
|
+
// A box cannot be both a sketch and a bound statement. Silently letting
|
|
1306
|
+
// one win renders a real, traceable step as scenery — or the reverse — and
|
|
1307
|
+
// the author who wrote both has no way to see which they got.
|
|
1308
|
+
if (block.sketchIds.has(binding.nodeId)) {
|
|
1309
|
+
errors.push(
|
|
1310
|
+
`Docflow box "${binding.nodeId}" is declared \`class ${binding.nodeId} sketch\` and also bound by \`// @mermaid-node ${binding.nodeId}\` on line ${binding.line}. It is one or the other: drop it from the class line, or drop the annotation.`,
|
|
1311
|
+
);
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1230
1314
|
if (binding.kind) node.kind = binding.kind;
|
|
1231
1315
|
}
|
|
1232
1316
|
projectRecordedArms(block, errors);
|
|
1233
|
-
if (block.syntax !== 'docflow') continue;
|
|
1234
1317
|
const bound = new Set(block.bindings.map((binding) => binding.nodeId));
|
|
1235
1318
|
for (const node of block.nodes.values()) {
|
|
1236
|
-
if (
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
)
|
|
1240
|
-
|
|
1319
|
+
if (
|
|
1320
|
+
node.sketch ||
|
|
1321
|
+
node.kind === 'conceptual' ||
|
|
1322
|
+
bound.has(node.id) ||
|
|
1323
|
+
rejectedNodeIds.has(node.id)
|
|
1324
|
+
)
|
|
1325
|
+
continue;
|
|
1326
|
+
errors.push(
|
|
1327
|
+
block.syntax === 'docflow'
|
|
1328
|
+
? `Docflow node "${node.id}" has no code binding; mark it type:"conceptual" or bind it.`
|
|
1329
|
+
: `Docflow box "${node.id}" in \`${blockLabel(block)}\` points at nothing, so the canvas can only draw it and say nothing about it. ` +
|
|
1330
|
+
`Either put \`// @mermaid-node ${node.id}\` above the statement it runs, or — if this play has no such statement, because the work happens in another module or inside a loop — add it to a \`class ${node.id} sketch\` line in the block to declare it a sketch.`,
|
|
1331
|
+
);
|
|
1241
1332
|
}
|
|
1242
1333
|
}
|
|
1243
1334
|
}
|
|
@@ -1292,6 +1383,7 @@ export function parsePlayDocflow(
|
|
|
1292
1383
|
function parseDocflowBindings(
|
|
1293
1384
|
sourceCode: string,
|
|
1294
1385
|
errors: string[],
|
|
1386
|
+
rejectedNodeIds: Set<string>,
|
|
1295
1387
|
): PlayDocflowBinding[] {
|
|
1296
1388
|
const lines = sourceCode.split(/\r?\n/);
|
|
1297
1389
|
const bindings: PlayDocflowBinding[] = [];
|
|
@@ -1302,6 +1394,11 @@ function parseDocflowBindings(
|
|
|
1302
1394
|
const legacyMatch = PUT.exec(lines[index]!);
|
|
1303
1395
|
const mermaidMatch = MERMAID_NODE.exec(lines[index]!);
|
|
1304
1396
|
if (!legacyMatch && !mermaidMatch) continue;
|
|
1397
|
+
// Whatever this annotation names, the author has now written it down. Every
|
|
1398
|
+
// `continue` below is a rejection, and a rejected id must not come back as
|
|
1399
|
+
// an unbound box — see `rejectedNodeIds` in `parsePlayDocflowFile`.
|
|
1400
|
+
const annotatedId = mermaidMatch?.[1]?.trim();
|
|
1401
|
+
if (annotatedId) rejectedNodeIds.add(annotatedId);
|
|
1305
1402
|
const attributes = parseAttributes(
|
|
1306
1403
|
mermaidMatch ? (mermaidMatch[2] ?? '') : legacyMatch![1]!,
|
|
1307
1404
|
{
|
|
@@ -1311,11 +1408,12 @@ function parseDocflowBindings(
|
|
|
1311
1408
|
},
|
|
1312
1409
|
);
|
|
1313
1410
|
if (!attributes) continue;
|
|
1314
|
-
const id = (
|
|
1411
|
+
const id = (annotatedId ?? attributes.id)?.trim();
|
|
1315
1412
|
if (!id) {
|
|
1316
1413
|
errors.push(`Docflow annotation on line ${index + 1} requires id:"…".`);
|
|
1317
1414
|
continue;
|
|
1318
1415
|
}
|
|
1416
|
+
rejectedNodeIds.add(id);
|
|
1319
1417
|
const kind = nodeKind(attributes.type);
|
|
1320
1418
|
if (attributes.type && !kind) {
|
|
1321
1419
|
errors.push(
|
|
@@ -1370,6 +1468,7 @@ function parseDocflowBindings(
|
|
|
1370
1468
|
ioConfidence: 'explicit' as const,
|
|
1371
1469
|
}
|
|
1372
1470
|
: inferBindingIo(lines[nextLine]!);
|
|
1471
|
+
rejectedNodeIds.delete(id);
|
|
1373
1472
|
bindings.push({
|
|
1374
1473
|
nodeId: id,
|
|
1375
1474
|
line: nextLine + 1,
|