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
@@ -205,8 +205,21 @@ function resolvePlayRunRuntimeSelection(
205
205
  process.env.DEEPLINE_RUNTIME_ENVIRONMENT_TOKEN?.trim();
206
206
  if (!configured) {
207
207
  if (configuredNamespace || configuredToken) {
208
+ // Name the variables that are actually set. This branch is almost always
209
+ // hit by a stale value auto-loaded from a .env/.env.local rather than by
210
+ // a deliberate export, so the message has to say where to look and how
211
+ // to run the command anyway.
212
+ const present: string[] = [];
213
+ if (configuredNamespace) present.push('DEEPLINE_RUNTIME_NAMESPACE');
214
+ if (configuredToken) present.push('DEEPLINE_RUNTIME_ENVIRONMENT_TOKEN');
215
+ const isPlural = present.length > 1;
208
216
  throw new DeeplineError(
209
- 'DEEPLINE_RUNTIME_ENVIRONMENT=preview and DEEPLINE_RUNTIME_NAMESPACE are required when runtime selection configuration is present.',
217
+ [
218
+ `Incomplete play runtime selection: ${present.join(' and ')} ${isPlural ? 'are' : 'is'} set but DEEPLINE_RUNTIME_ENVIRONMENT is not.`,
219
+ 'Preview routing requires DEEPLINE_RUNTIME_ENVIRONMENT=preview together with DEEPLINE_RUNTIME_NAMESPACE; the token alone authorizes nothing.',
220
+ `If you did not export ${isPlural ? 'these' : 'this'} yourself, ${isPlural ? 'they were' : 'it was'} most likely auto-loaded from a .env or .env.local in the current directory (bun loads those automatically, and in a git worktree .env.local is usually a symlink to the main checkout).`,
221
+ `To run against the app-native runtime, clear ${isPlural ? 'them' : 'it'} for this command: ${present.map((name) => `${name}=`).join(' ')} <your deepline command>`,
222
+ ].join('\n'),
210
223
  undefined,
211
224
  'INVALID_RUNTIME_ENVIRONMENT',
212
225
  );
@@ -2448,6 +2461,9 @@ export class DeeplineClient {
2448
2461
  sourceFiles?: Record<string, string>;
2449
2462
  description?: string;
2450
2463
  artifact: Record<string, unknown>;
2464
+ /** Which exported play in `sourceCode` this artifact is, when the file
2465
+ * exports more than one. Omit for the default export. */
2466
+ exportName?: string | null;
2451
2467
  integrationMode?: 'live' | 'eval_stub' | 'fixture';
2452
2468
  /**
2453
2469
  * Sibling plays from the same local bundle graph. Lets the server splice
@@ -39,7 +39,7 @@ export type {
39
39
 
40
40
  export { extractDefinedPlayName } from '../../../shared_libs/plays/bundling/index.js';
41
41
 
42
- const PLAY_BUNDLE_CACHE_VERSION = 31;
42
+ const PLAY_BUNDLE_CACHE_VERSION = 33;
43
43
  const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
44
44
  const SDK_PACKAGE_ROOT = resolve(MODULE_DIR, '..', '..');
45
45
  const SOURCE_REPO_ROOT = resolve(SDK_PACKAGE_ROOT, '..');
@@ -160,7 +160,7 @@ export const SDK_RELEASE = {
160
160
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
161
161
  // exposed storage-dependent synchronous access. This deliberate minor
162
162
  // release keeps lazy paging semantics independent of row residency.
163
- version: '0.2.53',
163
+ version: '0.2.55',
164
164
  contracts: {
165
165
  api: {
166
166
  name: 'sdk-http-api',
@@ -1270,6 +1270,14 @@ export interface PlayCheckResult {
1270
1270
  graphHash?: string | null;
1271
1271
  /** SHA-256 of the exact source bytes checked by Deepline. */
1272
1272
  sourceHash?: string | null;
1273
+ /**
1274
+ * Per-export results, present ONLY when the checked file exports more than
1275
+ * one play. Single-play files keep the exact flat shape they always had.
1276
+ * Entry 0 is the default export and mirrors the top-level fields; the
1277
+ * top-level `valid` is the AND over every entry, so an agent that reads only
1278
+ * `valid` can never ship a file whose second play fails.
1279
+ */
1280
+ exports?: PlayCheckExportResult[];
1273
1281
  /** Enforceable byte budgets measured during cloud preflight. */
1274
1282
  limits?: {
1275
1283
  revisionStorage: {
@@ -1291,6 +1299,29 @@ export interface PlayCheckResult {
1291
1299
  };
1292
1300
  }
1293
1301
 
1302
+ /**
1303
+ * One exported play's check result inside a multi-play file. Carries the same
1304
+ * per-play fields as {@link PlayCheckResult}, unprefixed and unaggregated, so a
1305
+ * consumer can attribute an error to the export that produced it.
1306
+ */
1307
+ export interface PlayCheckExportResult {
1308
+ /** Canonical export name: `default`, or the named export (`batch`). */
1309
+ exportName: string;
1310
+ /** The `definePlay` name this export declares. */
1311
+ name?: string | null;
1312
+ valid: boolean;
1313
+ errors: string[];
1314
+ warnings?: string[];
1315
+ issues?: PlayCheckIssue[];
1316
+ staticPipeline?: Record<string, unknown> | null;
1317
+ artifactHash?: string | null;
1318
+ graphHash?: string | null;
1319
+ sourceHash?: string | null;
1320
+ summary?: string;
1321
+ recognized?: PlayCheckRecognizedSummary;
1322
+ triggers?: PlayCheckTriggersSummary | null;
1323
+ }
1324
+
1294
1325
  /** Severity of a {@link PlayCheckIssue}. `error` fails the check; `warning` does not. */
1295
1326
  export type PlayCheckIssueSeverity = 'error' | 'warning';
1296
1327
 
@@ -1305,6 +1336,12 @@ export interface PlayCheckIssue {
1305
1336
  code: string;
1306
1337
  severity: PlayCheckIssueSeverity;
1307
1338
  message: string;
1339
+ /**
1340
+ * Which exported play raised this issue, when the checked file exports more
1341
+ * than one and this is not the default export. Absent everywhere else, so
1342
+ * the structured channel stays byte-identical for single-play files.
1343
+ */
1344
+ exportName?: string;
1308
1345
  path?: string;
1309
1346
  hint?: string;
1310
1347
  validOptions?: string[];
@@ -1318,7 +1355,12 @@ export interface PlayCheckIssue {
1318
1355
  export interface PlayCheckRecognizedSummary {
1319
1356
  triggers?: PlayCheckTriggersSummary;
1320
1357
  tools?: string[];
1321
- datasets?: { name: string; columns?: string[] }[];
1358
+ /**
1359
+ * Durable datasets the play produces. `undrawnColumns` echoes the columns the
1360
+ * author declared out of an authored `@mermaid` diagram with
1361
+ * `.run({ undrawnColumns: [...] })`, so an opt-out is visible in check output.
1362
+ */
1363
+ datasets?: { name: string; columns?: string[]; undrawnColumns?: string[] }[];
1322
1364
  inputs?: string[];
1323
1365
  outputs?: string[];
1324
1366
  }
@@ -5,6 +5,7 @@ import { pipeline } from 'node:stream/promises';
5
5
  import type { PlaySheetContract } from '@shared_libs/plays/static-pipeline';
6
6
  import type { PlayRowUpdate } from '@shared_libs/play-runtime/ctx-types';
7
7
  import type { PlayRunTimelineEntry } from '@shared_libs/play-runtime/live-events';
8
+ import type { PlayDatasetBornFrom } from '@shared_libs/play-runtime/cell-provenance';
8
9
  import {
9
10
  routePlayActivityObservation,
10
11
  type PlayActivityObservation,
@@ -84,6 +85,8 @@ export type RuntimeStatusUpdate = {
84
85
  succeededRows?: number;
85
86
  failedRows?: number;
86
87
  complete?: boolean;
88
+ /** Dataset-grain row birth, stated once per dataset (ADR 0019). */
89
+ bornFrom?: PlayDatasetBornFrom;
87
90
  at: number;
88
91
  }>;
89
92
  /** Sparse typed activity facts. Routing/deduplication happens server-side. */
@@ -1432,7 +1435,13 @@ async function updateRunStatusViaAppRuntimeUnlocked(
1432
1435
  (event.persistedRows ?? 0) > current.persistedRows ||
1433
1436
  (event.succeededRows ?? 0) > current.succeededRows ||
1434
1437
  (event.failedRows ?? 0) > current.failedRows ||
1435
- (event.complete === true && current.complete !== true);
1438
+ (event.complete === true && current.complete !== true) ||
1439
+ // A birth record the snapshot has not seen — or a larger admitted-row
1440
+ // count from a later page — is itself a change, or the dataset would keep
1441
+ // an unattributed or stale birth for the whole run.
1442
+ (event.bornFrom !== undefined &&
1443
+ (!current.bornFrom ||
1444
+ event.bornFrom.rowCountIn > current.bornFrom.rowCountIn));
1436
1445
  if (!changesSnapshot) continue;
1437
1446
  events.push({
1438
1447
  type: 'dataset.lifecycle',
@@ -1453,6 +1462,7 @@ async function updateRunStatusViaAppRuntimeUnlocked(
1453
1462
  ? {}
1454
1463
  : { failedRows: event.failedRows }),
1455
1464
  ...(event.complete === undefined ? {} : { complete: event.complete }),
1465
+ ...(event.bornFrom === undefined ? {} : { bornFrom: event.bornFrom }),
1456
1466
  });
1457
1467
  }
1458
1468
  const latestActivities = new Map(
@@ -1908,3 +1918,22 @@ export async function skipRuntimeStepReceiptViaAppRuntime(
1908
1918
  ...input,
1909
1919
  });
1910
1920
  }
1921
+
1922
+ /**
1923
+ * Post one durable docflow observation to the receipt gateway (ADR 0016 rule
1924
+ * 2). Callers invoke this fire-and-forget; it resolves on the gateway ack and
1925
+ * surfaces transport failures to its caller (the runner's observation sink),
1926
+ * which swallows-and-logs so the play body is never affected.
1927
+ */
1928
+ export async function observeDocflowNodeViaAppRuntime(
1929
+ context: WorkerRuntimeApiContext,
1930
+ input: Omit<
1931
+ Extract<RuntimeApiRequest, { action: 'observe_docflow_node' }>,
1932
+ 'action'
1933
+ >,
1934
+ ): Promise<void> {
1935
+ await postAppRuntimeApi<{ ok?: boolean }>(context, {
1936
+ action: 'observe_docflow_node',
1937
+ ...input,
1938
+ });
1939
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Cell-level provenance capture (ADR 0019).
3
+ *
4
+ * ADR 0018 answered *which producer* filled a cell. This module carries the
5
+ * other half — *what that producer was given*, *which branch was taken*, and
6
+ * *where the row came from* — as additive fields on writes the runtime already
7
+ * makes. Nothing here performs a round trip, and every value is a fact the
8
+ * runtime held at the write site rather than a declaration it trusted or a
9
+ * mapping it reconstructed afterwards.
10
+ *
11
+ * Shared by the runtime (`ctx-types`/`context`), the run ledger, and the
12
+ * durable sheet parse path, so all three agree on one shape and one bound.
13
+ */
14
+
15
+ /**
16
+ * One cell version a cell was computed from.
17
+ *
18
+ * Both addressing components are omitted when they match the cell that carries
19
+ * the ref: no `table` means the same table, no `rowKey` means the same row.
20
+ * That is not shorthand for the reader's convenience — it is what keeps the
21
+ * trace inside the retained-row memory budget. Runtime sheet row keys are
22
+ * namespace-salted hashes, and repeating one per ref per cell inflated a
23
+ * mapped row by roughly forty percent, which the streaming-dataset budget test
24
+ * rejects. Readers fill both in from the cell's own address; the durable sheet
25
+ * read model does this so every consumer sees a complete ref.
26
+ *
27
+ * `version` is the attempt/fence identity of the value read and is set only
28
+ * where the runtime already held one: absent means "not witnessed", never
29
+ * "version 0".
30
+ */
31
+ export interface PlayCellReadRef {
32
+ table?: string;
33
+ rowKey?: string;
34
+ column: string;
35
+ version?: number;
36
+ }
37
+
38
+ /**
39
+ * The branch a per-row control-flow evaluation selected, with the read-set the
40
+ * evaluator was handed. `branch` is the runtime's own vocabulary, never
41
+ * authored free text.
42
+ */
43
+ export interface PlayCellDecision {
44
+ branch: string;
45
+ reads: PlayCellReadRef[];
46
+ }
47
+
48
+ /** Row-grain birth: the source row the runtime held while building this row. */
49
+ export type PlayRowBornFromRow = { table: string; rowKey: string };
50
+
51
+ /**
52
+ * Dataset-grain birth: rows arrived from free-form play-body code and no
53
+ * per-row mapping was witnessed. It names the destination table and how many
54
+ * rows entered, and deliberately says nothing about any single row.
55
+ *
56
+ * There is no `step` field. The only step name available at the write site is
57
+ * `map:<table>`, which restates `table` — a derived join key, not an observed
58
+ * fact — and the record already hangs off the dataset's own lifecycle entry.
59
+ */
60
+ export type PlayDatasetBornFrom = {
61
+ table: string;
62
+ rowCountIn: number;
63
+ };
64
+
65
+ export type PlayRowBornFrom = PlayRowBornFromRow | PlayDatasetBornFrom;
66
+
67
+ /**
68
+ * Per-cell cap on the durable read trace, matching the producer trace bound.
69
+ * The read-set is the row's column list, so an unbounded trace would put a wide
70
+ * row's whole schema into `_cell_meta` once per cell. Truncation keeps the
71
+ * first columns in row order, which is stable across runs.
72
+ */
73
+ export const MAX_CELL_READ_REFS = 12;
74
+
75
+ /**
76
+ * Cap on how many cells one row's read order describes. A dataset has a handful
77
+ * of columns; this only bounds pathological graphs, and it reports what it
78
+ * dropped rather than trimming silently.
79
+ */
80
+ export const MAX_ROW_READ_CELLS = 24;
81
+
82
+ /** Reserved `_cell_meta` key holding row-grain facts rather than a cell. */
83
+ export const ROW_META_CELL_KEY = '_row';
84
+
85
+ /**
86
+ * Row-grain read order (ADR 0019).
87
+ *
88
+ * Every cell in a row reads a prefix of the same list: the row's input columns,
89
+ * then each column this run wrote, in the order they became available. Storing
90
+ * the list once per row and a cut point per cell is the same information as a
91
+ * per-cell array at O(columns) instead of O(columns x cells) — which matters,
92
+ * because this rides the retained-row memory budget on every mapped row.
93
+ *
94
+ * A written column is appended to `columns` before its own cell runs, so its
95
+ * position *is* its cut point: everything before it was available, itself and
96
+ * everything after it was not. `upto` therefore only carries cells the list
97
+ * cannot place — non-persisted fields, and cells past the column cap.
98
+ * `droppedCells` counts cells no cut describes at all, so an incomplete order
99
+ * is visible instead of looking like a cell that read nothing.
100
+ */
101
+ export interface PlayRowReadOrder {
102
+ columns: string[];
103
+ upto?: Record<string, number>;
104
+ droppedCells?: number;
105
+ }
106
+
107
+ /**
108
+ * How many leading `columns` entries a cell read, or null when the order
109
+ * describes no read for it. Null is not zero: a cell that read nothing and a
110
+ * cell nobody recorded must not render the same.
111
+ */
112
+ export function cellReadCut(
113
+ order: PlayRowReadOrder | null | undefined,
114
+ column: string,
115
+ ): number | null {
116
+ if (!order) return null;
117
+ const explicit = order.upto?.[column];
118
+ if (typeof explicit === 'number' && Number.isFinite(explicit)) {
119
+ return Math.max(0, Math.min(Math.trunc(explicit), order.columns.length));
120
+ }
121
+ const index = order.columns.indexOf(column);
122
+ return index >= 0 ? index : null;
123
+ }
124
+
125
+ /** Row-grain `_cell_meta._row` record. */
126
+ export interface PlayRowMeta {
127
+ reads?: PlayRowReadOrder;
128
+ bornFrom?: PlayRowBornFromRow;
129
+ }
130
+
131
+ /**
132
+ * Expand one cell's read-set out of the row order. Returns undefined when the
133
+ * order describes no read for that cell, which is not the same as an empty
134
+ * read-set and must not be rendered as one.
135
+ */
136
+ export function hydrateCellReadRefs(
137
+ order: PlayRowReadOrder | null | undefined,
138
+ column: string,
139
+ rowKey: string,
140
+ ): Array<PlayCellReadRef & { rowKey: string }> | undefined {
141
+ const cut = cellReadCut(order, column);
142
+ if (cut === null || cut <= 0 || !order) return undefined;
143
+ const refs = order.columns
144
+ .slice(0, Math.min(cut, MAX_CELL_READ_REFS))
145
+ .map((readColumn) => ({ rowKey, column: readColumn }));
146
+ return refs.length > 0 ? refs : undefined;
147
+ }
148
+
149
+ /** Parse a stored row read order off untrusted transport. */
150
+ export function normalizeRowReadOrder(value: unknown): PlayRowReadOrder | null {
151
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
152
+ const record = value as Record<string, unknown>;
153
+ const columns = Array.isArray(record.columns)
154
+ ? record.columns
155
+ .filter((column): column is string => typeof column === 'string')
156
+ .slice(0, MAX_CELL_READ_REFS)
157
+ : [];
158
+ if (columns.length === 0) return null;
159
+ const rawUpto =
160
+ record.upto &&
161
+ typeof record.upto === 'object' &&
162
+ !Array.isArray(record.upto)
163
+ ? (record.upto as Record<string, unknown>)
164
+ : {};
165
+ const upto: Record<string, number> = {};
166
+ let entries = 0;
167
+ for (const [column, cut] of Object.entries(rawUpto)) {
168
+ if (entries >= MAX_ROW_READ_CELLS) break;
169
+ if (typeof cut !== 'number' || !Number.isFinite(cut) || cut < 0) continue;
170
+ upto[column] = Math.min(Math.trunc(cut), columns.length);
171
+ entries += 1;
172
+ }
173
+ const droppedCells = finiteNonNegativeInteger(record.droppedCells);
174
+ return {
175
+ columns,
176
+ ...(entries > 0 ? { upto } : {}),
177
+ ...(droppedCells ? { droppedCells } : {}),
178
+ };
179
+ }
180
+
181
+ /** Enforce {@link MAX_CELL_READ_REFS} without reordering. */
182
+ export function boundCellReadRefs<T>(refs: readonly T[]): T[] {
183
+ return refs.length > MAX_CELL_READ_REFS
184
+ ? refs.slice(0, MAX_CELL_READ_REFS)
185
+ : [...refs];
186
+ }
187
+
188
+ /**
189
+ * Fold a newly stated dataset birth into the one already on the snapshot.
190
+ *
191
+ * A paged dataset registers more than once, each registration stating the rows
192
+ * admitted so far, so the larger `rowCountIn` is the later truth. A phase event
193
+ * that states no birth never erases one that was witnessed.
194
+ */
195
+ export function mergeDatasetBornFrom(
196
+ current: PlayDatasetBornFrom | null | undefined,
197
+ next: PlayDatasetBornFrom | null | undefined,
198
+ ): PlayDatasetBornFrom | null {
199
+ if (!next) return current ?? null;
200
+ if (!current) return next;
201
+ return next.rowCountIn > current.rowCountIn ? next : current;
202
+ }
203
+
204
+ function finiteNonNegativeInteger(value: unknown): number | null {
205
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
206
+ return null;
207
+ }
208
+ return Math.trunc(value);
209
+ }
210
+
211
+ function nonEmptyString(value: unknown): string | null {
212
+ if (typeof value !== 'string') return null;
213
+ const trimmed = value.trim();
214
+ return trimmed ? trimmed : null;
215
+ }
216
+
217
+ /**
218
+ * Parse a dataset-grain birth record off untrusted transport. Returns null for
219
+ * anything that is not a complete record: a partial birth record would claim a
220
+ * lineage the writer never stated.
221
+ */
222
+ export function normalizeDatasetBornFrom(
223
+ value: unknown,
224
+ ): PlayDatasetBornFrom | null {
225
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
226
+ const record = value as Record<string, unknown>;
227
+ const table = nonEmptyString(record.table);
228
+ const rowCountIn = finiteNonNegativeInteger(record.rowCountIn);
229
+ if (!table || rowCountIn === null) return null;
230
+ return { table, rowCountIn };
231
+ }