deepline 0.3.227 → 0.3.229

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 (34) hide show
  1. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  2. package/dist/bundling-sources/shared_libs/play-data-plane/contracts.ts +12 -0
  3. package/dist/bundling-sources/shared_libs/play-data-plane/index.ts +1 -0
  4. package/dist/bundling-sources/shared_libs/play-data-plane/sheet-column-storage.ts +252 -0
  5. package/dist/bundling-sources/shared_libs/play-data-plane/sheet-contract.ts +29 -8
  6. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +250 -37
  7. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +2 -0
  8. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-event-transport.ts +29 -0
  9. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-projection-diagnostics.ts +43 -1
  10. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +6 -1
  11. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +380 -183
  12. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +17 -28
  13. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +1 -0
  14. package/dist/bundling-sources/shared_libs/plays/dataset.ts +16 -2
  15. package/dist/bundling-sources/shared_libs/plays/execution-pipeline.ts +72 -0
  16. package/dist/bundling-sources/shared_libs/plays/launch/prepare.ts +1 -3
  17. package/dist/bundling-sources/shared_libs/plays/sheet-filter.ts +9 -13
  18. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +28 -31
  19. package/dist/cli/index.js +1 -1
  20. package/dist/cli/index.mjs +1 -1
  21. package/dist/{compiler-manifest-Dpd2hEet.d.mts → compiler-manifest-n8ZtPv2h.d.mts} +31 -21
  22. package/dist/{compiler-manifest-Dpd2hEet.d.ts → compiler-manifest-n8ZtPv2h.d.ts} +31 -21
  23. package/dist/index.d.mts +2 -2
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.js +6 -1
  26. package/dist/index.mjs +6 -1
  27. package/dist/install-integrity.json +4 -2
  28. package/dist/plays/bundle-play-file.d.mts +2 -2
  29. package/dist/plays/bundle-play-file.d.ts +2 -2
  30. package/dist/release.d.mts +1 -1
  31. package/dist/release.d.ts +1 -1
  32. package/dist/release.js +1 -1
  33. package/dist/release.mjs +1 -1
  34. package/package.json +1 -1
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
202
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
203
- version: '0.3.227',
203
+ version: '0.3.229',
204
204
  updateSummary:
205
205
  'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
206
206
  packageCapabilities: {
@@ -1,3 +1,14 @@
1
+ /** Source evidence is advisory; stored values determine lossless SQL encoding. */
2
+ export interface PlayDatasetColumnTypeEvidence {
3
+ postgresTypeId: number;
4
+ valueType: 'boolean' | 'number' | 'string' | 'json';
5
+ }
6
+
7
+ export type PlayDatasetColumnTypes = Record<
8
+ string,
9
+ PlayDatasetColumnTypeEvidence
10
+ >;
11
+
1
12
  export type PlaySheetColumnSource =
2
13
  | 'input'
3
14
  | 'datasetColumn'
@@ -17,6 +28,7 @@ export interface PlaySheetColumnContract {
17
28
  stepId?: string;
18
29
  toolId?: string;
19
30
  isRowKey?: boolean;
31
+ typeEvidence?: PlayDatasetColumnTypeEvidence;
20
32
  }
21
33
 
22
34
  export interface PlaySheetContract {
@@ -3,3 +3,4 @@ export * from './contracts';
3
3
  export * from './sheet-contract';
4
4
  export * from './runtime-sheet-storage-contract';
5
5
  export * from './runtime-sheet-orm';
6
+ export * from './sheet-column-storage';
@@ -0,0 +1,252 @@
1
+ import type { PhysicalSheetColumnProjection } from './sheet-contract';
2
+
3
+ type QueryClient = {
4
+ query(
5
+ sql: string,
6
+ params?: unknown[],
7
+ ): Promise<{ rows: Record<string, unknown>[] }>;
8
+ };
9
+
10
+ type SheetIdentity = { schema: string; table: string };
11
+ type StorageType = 'boolean' | 'double precision' | 'text' | 'jsonb';
12
+ type ColumnState = { name: string; type: string; marker: string | null };
13
+ export type RuntimeSheetColumnTypes = ReadonlyMap<string, string>;
14
+
15
+ const MARKER = 'deepline:runtime-sheet-column:';
16
+ const UNDECIDED = `${MARKER}undecided`;
17
+ const NATIVE = `${MARKER}native`;
18
+ const MIXED = `${MARKER}jsonb`;
19
+
20
+ const identifier = (value: string) => `"${value.replaceAll('"', '""')}"`;
21
+ const literal = (value: string) => `'${value.replaceAll("'", "''")}'`;
22
+ const relation = (sheet: SheetIdentity) =>
23
+ `${identifier(sheet.schema)}.${identifier(sheet.table)}`;
24
+
25
+ function combine(
26
+ a: StorageType | null,
27
+ b: StorageType | null,
28
+ ): StorageType | null {
29
+ return a === null ? b : b === null || a === b ? a : 'jsonb';
30
+ }
31
+
32
+ function valueType(value: unknown): StorageType | null {
33
+ if (value == null) return null;
34
+ if (typeof value === 'boolean') return 'boolean';
35
+ if (typeof value === 'string') return 'text';
36
+ if (typeof value === 'number' && Number.isFinite(value))
37
+ return 'double precision';
38
+ return 'jsonb';
39
+ }
40
+
41
+ async function lock(client: QueryClient, sheet: SheetIdentity): Promise<void> {
42
+ // The caller holds this transaction through DML. Both SQL writers use the
43
+ // same lock, so type inspection, widening and the affected write are atomic.
44
+ await client.query('SELECT pg_advisory_xact_lock(hashtextextended($1, 0))', [
45
+ `runtime-sheet-columns:${relation(sheet)}`,
46
+ ]);
47
+ }
48
+
49
+ async function inspect(
50
+ client: QueryClient,
51
+ sheet: SheetIdentity,
52
+ ): Promise<ColumnState[]> {
53
+ const result = await client.query(
54
+ `SELECT a.attname AS name, format_type(a.atttypid, NULL) AS type,
55
+ col_description(a.attrelid, a.attnum) AS marker
56
+ FROM pg_attribute a
57
+ WHERE a.attrelid = to_regclass($1)
58
+ AND a.attnum > 0 AND NOT a.attisdropped`,
59
+ [relation(sheet)],
60
+ );
61
+ return result.rows.map((row) => ({
62
+ name: String(row.name),
63
+ type: String(row.type),
64
+ marker: typeof row.marker === 'string' ? row.marker : null,
65
+ }));
66
+ }
67
+
68
+ async function mark(
69
+ client: QueryClient,
70
+ sheet: SheetIdentity,
71
+ column: string,
72
+ marker: string,
73
+ ) {
74
+ await client.query(
75
+ `COMMENT ON COLUMN ${relation(sheet)}.${identifier(column)} IS ${literal(marker)}`,
76
+ );
77
+ }
78
+
79
+ async function changeType(
80
+ client: QueryClient,
81
+ sheet: SheetIdentity,
82
+ column: ColumnState,
83
+ desired: StorageType | null,
84
+ ): Promise<void> {
85
+ if (desired === null || column.marker === MIXED) return;
86
+ // Unmarked columns predate this feature (or belong to customers). Never
87
+ // reinterpret them merely because their current SQL type happens to match.
88
+ if (column.marker !== UNDECIDED && column.marker !== NATIVE) return;
89
+ let next =
90
+ column.marker === UNDECIDED
91
+ ? desired
92
+ : combine(column.type as StorageType, desired)!;
93
+ if (column.marker === NATIVE && next === column.type) return;
94
+ const quoted = identifier(column.name);
95
+
96
+ if (column.marker === UNDECIDED && next !== 'jsonb') {
97
+ // Another compatible old writer may have filled a provisioned JSONB
98
+ // column before its first inference. Prove the whole stored column rather
99
+ // than relying on a preview or assuming the new marker means no data.
100
+ const jsonType =
101
+ next === 'text' ? 'string' : next === 'boolean' ? 'boolean' : 'number';
102
+ const incompatible = await client.query(
103
+ `SELECT EXISTS(SELECT 1 FROM ${relation(sheet)}
104
+ WHERE ${quoted} IS NOT NULL AND ${quoted} <> 'null'::jsonb
105
+ AND jsonb_typeof(${quoted}) <> $1) AS incompatible`,
106
+ [jsonType],
107
+ );
108
+ if (incompatible.rows[0]?.incompatible === true) next = 'jsonb';
109
+ if (next === 'double precision') {
110
+ // Existing arbitrary JSON numeric values may exceed IEEE-754 precision.
111
+ // CASE guards the cast; SQL predicate evaluation order is not a guard.
112
+ const numeric = `(${quoted} #>> '{}')::numeric`;
113
+ const loss = await client.query(
114
+ `SELECT EXISTS(SELECT 1 FROM ${relation(sheet)}
115
+ WHERE ${quoted} IS NOT NULL AND ${quoted} <> 'null'::jsonb
116
+ AND CASE WHEN abs(${numeric}) > 1.7976931348623157e308::numeric
117
+ OR (${numeric} <> 0 AND abs(${numeric}) < 5e-324::numeric)
118
+ THEN true
119
+ ELSE to_jsonb((${quoted} #>> '{}')::double precision) <> ${quoted}
120
+ END) AS incompatible`,
121
+ );
122
+ if (loss.rows[0]?.incompatible === true) next = 'jsonb';
123
+ }
124
+ }
125
+
126
+ if (next !== column.type) {
127
+ const conversion =
128
+ next === 'jsonb'
129
+ ? `to_jsonb(${quoted})`
130
+ : `(${quoted} #>> '{}')::${next}`;
131
+ await client.query(
132
+ `ALTER TABLE ${relation(sheet)} ALTER COLUMN ${quoted} TYPE ${next} USING ${conversion}`,
133
+ );
134
+ }
135
+ await mark(client, sheet, column.name, next === 'jsonb' ? MIXED : NATIVE);
136
+ column.type = next;
137
+ column.marker = next === 'jsonb' ? MIXED : NATIVE;
138
+ }
139
+
140
+ /**
141
+ * Runtime Sheet column storage owns conservative, monotonic SQL typing.
142
+ * Methods that mutate storage require an existing transaction, held through
143
+ * the corresponding row write. JSON values remain the transport contract.
144
+ */
145
+ export const RuntimeSheetColumns = {
146
+ lockForWrite: lock,
147
+ async markNewColumns(
148
+ client: QueryClient,
149
+ input: SheetIdentity & { columns: readonly string[] },
150
+ ): Promise<void> {
151
+ // Caller proves these columns were created in its schema transaction.
152
+ for (const column of input.columns)
153
+ await mark(client, input, column, UNDECIDED);
154
+ },
155
+
156
+ async prepareWrite(
157
+ client: QueryClient,
158
+ input: SheetIdentity & {
159
+ rows:
160
+ | readonly Record<string, unknown>[]
161
+ | (() => Promise<readonly Record<string, unknown>[]>);
162
+ columns: readonly PhysicalSheetColumnProjection[];
163
+ },
164
+ ): Promise<RuntimeSheetColumnTypes> {
165
+ await lock(client, input);
166
+ const rows =
167
+ typeof input.rows === 'function' ? await input.rows() : input.rows;
168
+ const columns = new Map(
169
+ (await inspect(client, input)).map((column) => [column.name, column]),
170
+ );
171
+ for (const projection of rows.length > 0 ? input.columns : []) {
172
+ const column = columns.get(projection.sqlName);
173
+ if (!column)
174
+ throw new Error(
175
+ `Runtime Sheet column is missing: ${projection.sqlName}`,
176
+ );
177
+ let desired: StorageType | null =
178
+ projection.typeEvidence?.valueType === 'json' ? 'jsonb' : null;
179
+ for (const row of rows)
180
+ desired = combine(desired, valueType(row[projection.fieldName]));
181
+ await changeType(client, input, column, desired);
182
+ }
183
+ return new Map([...columns].map(([name, column]) => [name, column.type]));
184
+ },
185
+
186
+ async prepareClone(
187
+ client: QueryClient,
188
+ input: SheetIdentity & {
189
+ sourceTable: string;
190
+ columns: readonly string[];
191
+ columnMappings?: readonly {
192
+ sourceSqlName: string;
193
+ targetSqlName: string;
194
+ }[];
195
+ },
196
+ ): Promise<RuntimeSheetColumnTypes> {
197
+ const source = { schema: input.schema, table: input.sourceTable };
198
+ const sheets = [input, source].sort((a, b) =>
199
+ relation(a) < relation(b) ? -1 : relation(a) > relation(b) ? 1 : 0,
200
+ );
201
+ for (const sheet of sheets) await lock(client, sheet);
202
+ const sourceColumns = new Map(
203
+ (await inspect(client, source)).map((column) => [column.name, column]),
204
+ );
205
+ const targetColumns = new Map(
206
+ (await inspect(client, input)).map((column) => [column.name, column]),
207
+ );
208
+ const mappings =
209
+ input.columnMappings ??
210
+ input.columns.map((name) => ({
211
+ sourceSqlName: name,
212
+ targetSqlName: name,
213
+ }));
214
+ for (const mapping of mappings) {
215
+ const sourceColumn = sourceColumns.get(mapping.sourceSqlName);
216
+ const targetColumn = targetColumns.get(mapping.targetSqlName);
217
+ if (!sourceColumn || !targetColumn)
218
+ throw new Error(
219
+ `Runtime Sheet clone column is missing: ${mapping.sourceSqlName} -> ${mapping.targetSqlName}`,
220
+ );
221
+ const type = sourceColumn.type;
222
+ await changeType(
223
+ client,
224
+ input,
225
+ targetColumn,
226
+ type === 'boolean' || type === 'text' || type === 'double precision'
227
+ ? type
228
+ : 'jsonb',
229
+ );
230
+ }
231
+ return new Map(
232
+ [...targetColumns].map(([name, column]) => [name, column.type]),
233
+ );
234
+ },
235
+
236
+ jsonValueSql(
237
+ jsonExpression: string,
238
+ tableReference: string,
239
+ columnName: string,
240
+ types: RuntimeSheetColumnTypes,
241
+ ): string {
242
+ const type = types.get(columnName);
243
+ if (!type)
244
+ throw new Error(`Runtime Sheet column is missing: ${columnName}`);
245
+ // jsonb_populate_record maps JSON null to SQL NULL. A legacy JSONB cell
246
+ // must retain explicit JSON null, distinct from an omitted/SQL NULL cell.
247
+ if (type === 'jsonb') return jsonExpression;
248
+ // Resolve the actual row type inside PostgreSQL, never from a cached
249
+ // contract. This also preserves JSON scalar strings for legacy JSONB.
250
+ return `(jsonb_populate_record(NULL::${tableReference}, jsonb_build_object(${literal(columnName)}, ${jsonExpression}))).${identifier(columnName)}`;
251
+ },
252
+ };
@@ -1,4 +1,8 @@
1
- import type { PlaySheetContract } from './contracts';
1
+ import type {
2
+ PlayDatasetColumnTypeEvidence,
3
+ PlayDatasetColumnTypes,
4
+ PlaySheetContract,
5
+ } from './contracts';
2
6
  import {
3
7
  legacyPostgresTruncatedPlayColumnName,
4
8
  sqlSafePlayColumnName,
@@ -7,6 +11,7 @@ import {
7
11
  export type PhysicalSheetColumnProjection = {
8
12
  sqlName: string;
9
13
  fieldName: string;
14
+ typeEvidence?: PlayDatasetColumnTypeEvidence;
10
15
  };
11
16
 
12
17
  export type LegacyPhysicalSheetColumnBackfill = {
@@ -37,13 +42,11 @@ export function isDatasetPayloadField(field: string): boolean {
37
42
 
38
43
  export function physicalSheetColumnProjections(
39
44
  sheetContract: PlaySheetContract | null | undefined,
45
+ physicalColumns?: readonly string[],
40
46
  ): PhysicalSheetColumnProjection[] {
41
- if (!sheetContract) {
42
- return [];
43
- }
44
47
  const seen = new Set<string>();
45
48
  const columns: PhysicalSheetColumnProjection[] = [];
46
- for (const column of sheetContract.columns) {
49
+ for (const column of sheetContract?.columns ?? []) {
47
50
  const sqlName = column.sqlName.trim();
48
51
  const fieldName =
49
52
  column.source === 'input' || column.source === 'datasetColumn'
@@ -53,7 +56,18 @@ export function physicalSheetColumnProjections(
53
56
  continue;
54
57
  }
55
58
  seen.add(sqlName);
56
- columns.push({ sqlName, fieldName });
59
+ columns.push({
60
+ sqlName,
61
+ fieldName,
62
+ ...(column.typeEvidence ? { typeEvidence: column.typeEvidence } : {}),
63
+ });
64
+ }
65
+ // A prior run may have introduced fields absent from the current artifact.
66
+ // Keep their live storage projection so reads and writes do not hide them.
67
+ for (const sqlName of physicalColumns ?? []) {
68
+ if (!sqlName || sqlName.startsWith('_') || seen.has(sqlName)) continue;
69
+ seen.add(sqlName);
70
+ columns.push({ sqlName, fieldName: sqlName });
57
71
  }
58
72
  return columns;
59
73
  }
@@ -141,7 +155,11 @@ export function outputPhysicalSheetColumnProjections(
141
155
  continue;
142
156
  }
143
157
  seen.add(sqlName);
144
- columns.push({ sqlName, fieldName: column.field });
158
+ columns.push({
159
+ sqlName,
160
+ fieldName: column.field,
161
+ ...(column.typeEvidence ? { typeEvidence: column.typeEvidence } : {}),
162
+ });
145
163
  }
146
164
  return columns;
147
165
  }
@@ -158,6 +176,7 @@ export function augmentSheetContractWithDatasetFields(input: {
158
176
  contract: PlaySheetContract;
159
177
  rows: readonly Record<string, unknown>[];
160
178
  outputFields?: readonly string[];
179
+ columnTypes?: PlayDatasetColumnTypes;
161
180
  }): PlaySheetContract {
162
181
  const outputFields = new Set(input.outputFields ?? []);
163
182
  const candidateFields = new Set<string>();
@@ -198,7 +217,9 @@ export function augmentSheetContractWithDatasetFields(input: {
198
217
  existingDatasetPayloadFields.add(field);
199
218
  }
200
219
  existingSqlNames.add(sqlName);
201
- target.push(column);
220
+ const typeEvidence =
221
+ column.source === 'input' ? input.columnTypes?.[field] : undefined;
222
+ target.push(typeEvidence ? { ...column, typeEvidence } : column);
202
223
  };
203
224
 
204
225
  for (const column of input.contract.columns) {