deepline 0.1.12 → 0.1.20

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 (82) hide show
  1. package/README.md +14 -6
  2. package/dist/cli/index.js +1346 -717
  3. package/dist/cli/index.mjs +1342 -713
  4. package/dist/index.d.mts +199 -23
  5. package/dist/index.d.ts +199 -23
  6. package/dist/index.js +221 -14
  7. package/dist/index.mjs +221 -14
  8. package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +214 -77
  9. package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +85 -60
  10. package/dist/repo/apps/play-runner-workers/src/entry.ts +385 -66
  11. package/dist/repo/sdk/src/client.ts +237 -0
  12. package/dist/repo/sdk/src/config.ts +125 -8
  13. package/dist/repo/sdk/src/http.ts +29 -5
  14. package/dist/repo/sdk/src/play.ts +19 -36
  15. package/dist/repo/sdk/src/plays/bundle-play-file.ts +22 -8
  16. package/dist/repo/sdk/src/plays/local-file-discovery.ts +207 -160
  17. package/dist/repo/sdk/src/types.ts +25 -0
  18. package/dist/repo/sdk/src/version.ts +2 -2
  19. package/dist/repo/shared_libs/play-runtime/tool-result.ts +237 -145
  20. package/dist/repo/shared_libs/plays/bundling/index.ts +206 -229
  21. package/dist/repo/shared_libs/plays/dataset.ts +28 -0
  22. package/dist/repo/shared_libs/plays/row-identity.ts +59 -4
  23. package/package.json +5 -4
  24. package/dist/cli/index.js.map +0 -1
  25. package/dist/cli/index.mjs.map +0 -1
  26. package/dist/index.js.map +0 -1
  27. package/dist/index.mjs.map +0 -1
  28. package/dist/repo/apps/play-runner-workers/src/runtime/README.md +0 -21
  29. package/dist/repo/apps/play-runner-workers/src/runtime/batching.ts +0 -177
  30. package/dist/repo/apps/play-runner-workers/src/runtime/execution-plan.ts +0 -52
  31. package/dist/repo/apps/play-runner-workers/src/runtime/tool-batch.ts +0 -100
  32. package/dist/repo/sdk/src/cli/commands/auth.ts +0 -500
  33. package/dist/repo/sdk/src/cli/commands/billing.ts +0 -188
  34. package/dist/repo/sdk/src/cli/commands/csv.ts +0 -123
  35. package/dist/repo/sdk/src/cli/commands/db.ts +0 -119
  36. package/dist/repo/sdk/src/cli/commands/feedback.ts +0 -40
  37. package/dist/repo/sdk/src/cli/commands/org.ts +0 -117
  38. package/dist/repo/sdk/src/cli/commands/play.ts +0 -3441
  39. package/dist/repo/sdk/src/cli/commands/tools.ts +0 -687
  40. package/dist/repo/sdk/src/cli/dataset-stats.ts +0 -415
  41. package/dist/repo/sdk/src/cli/index.ts +0 -148
  42. package/dist/repo/sdk/src/cli/progress.ts +0 -149
  43. package/dist/repo/sdk/src/cli/skills-sync.ts +0 -141
  44. package/dist/repo/sdk/src/cli/trace.ts +0 -61
  45. package/dist/repo/sdk/src/cli/utils.ts +0 -145
  46. package/dist/repo/sdk/src/compat.ts +0 -77
  47. package/dist/repo/shared_libs/observability/node-tracing.ts +0 -129
  48. package/dist/repo/shared_libs/observability/tracing.ts +0 -98
  49. package/dist/repo/shared_libs/play-runtime/context.ts +0 -4242
  50. package/dist/repo/shared_libs/play-runtime/ctx-contract.ts +0 -250
  51. package/dist/repo/shared_libs/play-runtime/ctx-types.ts +0 -725
  52. package/dist/repo/shared_libs/play-runtime/dataset-id.ts +0 -10
  53. package/dist/repo/shared_libs/play-runtime/db-session-crypto.ts +0 -304
  54. package/dist/repo/shared_libs/play-runtime/db-session.ts +0 -462
  55. package/dist/repo/shared_libs/play-runtime/live-events.ts +0 -214
  56. package/dist/repo/shared_libs/play-runtime/live-state-contract.ts +0 -50
  57. package/dist/repo/shared_libs/play-runtime/map-execution-frame.ts +0 -114
  58. package/dist/repo/shared_libs/play-runtime/map-row-identity.ts +0 -158
  59. package/dist/repo/shared_libs/play-runtime/progress-emitter.ts +0 -172
  60. package/dist/repo/shared_libs/play-runtime/protocol.ts +0 -121
  61. package/dist/repo/shared_libs/play-runtime/public-play-contract.ts +0 -42
  62. package/dist/repo/shared_libs/play-runtime/result-normalization.ts +0 -33
  63. package/dist/repo/shared_libs/play-runtime/runtime-api.ts +0 -1873
  64. package/dist/repo/shared_libs/play-runtime/runtime-constraints.ts +0 -2
  65. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-neon-serverless.ts +0 -201
  66. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-pg.ts +0 -48
  67. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver.ts +0 -84
  68. package/dist/repo/shared_libs/play-runtime/static-pipeline-types.ts +0 -147
  69. package/dist/repo/shared_libs/play-runtime/suspension.ts +0 -68
  70. package/dist/repo/shared_libs/play-runtime/tracing.ts +0 -31
  71. package/dist/repo/shared_libs/play-runtime/waterfall-replay.ts +0 -75
  72. package/dist/repo/shared_libs/play-runtime/worker-api-types.ts +0 -140
  73. package/dist/repo/shared_libs/plays/artifact-transport.ts +0 -14
  74. package/dist/repo/shared_libs/plays/artifact-types.ts +0 -49
  75. package/dist/repo/shared_libs/plays/compiler-manifest.ts +0 -186
  76. package/dist/repo/shared_libs/plays/definition.ts +0 -264
  77. package/dist/repo/shared_libs/plays/file-refs.ts +0 -11
  78. package/dist/repo/shared_libs/plays/rate-limit-scheduler.ts +0 -206
  79. package/dist/repo/shared_libs/plays/resolve-static-pipeline.ts +0 -164
  80. package/dist/repo/shared_libs/plays/runtime-validation.ts +0 -395
  81. package/dist/repo/shared_libs/temporal/constants.ts +0 -39
  82. package/dist/repo/shared_libs/temporal/preview-config.ts +0 -153
@@ -1,11 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { existsSync } from 'node:fs';
3
2
  import { mkdir, readFile, realpath, stat, writeFile } from 'node:fs/promises';
4
3
  import { tmpdir } from 'node:os';
5
4
  import { basename, dirname, extname, isAbsolute, join, resolve } from 'node:path';
6
5
  import { builtinModules, createRequire } from 'node:module';
7
6
  import { build, type Message, type Plugin } from 'esbuild';
8
- import ts from 'typescript';
9
7
  import {
10
8
  PLAY_ARTIFACT_KINDS,
11
9
  type PlayArtifactKind,
@@ -90,7 +88,6 @@ export type PlayBundlingAdapter = {
90
88
  workersHarnessEntryFile: string;
91
89
  workersHarnessFilesDir: string;
92
90
  discoverPackagedLocalFiles(filePath: string): Promise<PlayLocalFileDiscoveryResult>;
93
- typecheckSdkTypes?: boolean;
94
91
  typecheckPlaySource?(input: {
95
92
  sourceCode: string;
96
93
  sourcePath: string;
@@ -112,6 +109,7 @@ export type BundledPlayFileSuccess = {
112
109
  success: true;
113
110
  artifact: PlayBundleArtifact;
114
111
  sourceCode: string;
112
+ sourceFiles: Record<string, string>;
115
113
  filePath: string;
116
114
  playName: string | null;
117
115
  compilerManifest?: PlayCompilerManifest;
@@ -137,6 +135,7 @@ type PackageResolution = {
137
135
 
138
136
  type SourceGraphAnalysis = {
139
137
  sourceCode: string;
138
+ sourceFiles: Record<string, string>;
140
139
  sourceHash: string;
141
140
  graphHash: string;
142
141
  importPolicy: PlayImportPolicy;
@@ -149,6 +148,13 @@ type PlayWorkspace = {
149
148
  rootDir: string;
150
149
  };
151
150
 
151
+ type SourceImportReference = {
152
+ specifier: string;
153
+ line: number;
154
+ column: number;
155
+ kind: 'static' | 'require' | 'dynamic-import';
156
+ };
157
+
152
158
  function sha256(value: string): string {
153
159
  return createHash('sha256').update(value).digest('hex');
154
160
  }
@@ -160,67 +166,6 @@ function formatEsbuildMessage(message: Message): string {
160
166
  return location ? `${location} ${message.text}` : message.text;
161
167
  }
162
168
 
163
- function formatTypeScriptDiagnostic(diagnostic: ts.Diagnostic): string | null {
164
- if (!diagnostic.file) {
165
- const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n').trim();
166
- return message || null;
167
- }
168
- const start = diagnostic.start ?? 0;
169
- const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(start);
170
- const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n').trim();
171
- if (!message) {
172
- return null;
173
- }
174
- return `${diagnostic.file.fileName}:${line + 1}:${character + 1} ${message}`;
175
- }
176
-
177
- function resolveBundledTypeRoots(): string[] {
178
- try {
179
- return [dirname(dirname(playArtifactRequire.resolve('@types/node/package.json')))];
180
- } catch {
181
- return [];
182
- }
183
- }
184
-
185
- function typecheckPlaySource(
186
- input: SourceGraphAnalysis,
187
- adapter: PlayBundlingAdapter,
188
- ): string[] {
189
- const rootNames = Array.from(
190
- new Set([
191
- ...input.importPolicy.localFiles,
192
- ...input.importedPlayDependencies.map((dependency) => dependency.filePath),
193
- ]),
194
- );
195
- // Resolve `deepline` to SDK source, not ignored dist artifacts. The source
196
- // package surface is the canonical local play authoring contract.
197
- const sdkTypesPath = adapter.sdkTypesEntryFile ?? adapter.sdkEntryFile;
198
- const program = ts.createProgram(rootNames, {
199
- target: ts.ScriptTarget.ES2023,
200
- // SDK source uses fetch/RequestInit/URL and node-aware config helpers.
201
- // The play runtime import policy below still bans Node modules from play
202
- // source for workers_edge bundles.
203
- lib: ['lib.es2023.d.ts', 'lib.dom.d.ts'],
204
- module: ts.ModuleKind.ESNext,
205
- moduleResolution: ts.ModuleResolutionKind.Bundler,
206
- paths: { deepline: [sdkTypesPath] },
207
- strict: true,
208
- skipLibCheck: true,
209
- noEmit: true,
210
- esModuleInterop: true,
211
- allowSyntheticDefaultImports: true,
212
- allowImportingTsExtensions: true,
213
- allowJs: true,
214
- resolveJsonModule: true,
215
- types: ['node'],
216
- typeRoots: resolveBundledTypeRoots(),
217
- });
218
- return ts
219
- .getPreEmitDiagnostics(program)
220
- .map(formatTypeScriptDiagnostic)
221
- .filter((message): message is string => Boolean(message));
222
- }
223
-
224
169
  function isLocalSpecifier(specifier: string): boolean {
225
170
  return (
226
171
  specifier.startsWith('./') ||
@@ -254,18 +199,15 @@ function assertWithinPlayWorkspace(input: {
254
199
  specifier: string;
255
200
  resolvedPath: string;
256
201
  workspace: PlayWorkspace;
257
- sourceFile: ts.SourceFile;
258
- node: ts.Node;
202
+ line: number;
203
+ column: number;
259
204
  }): void {
260
205
  if (isPathInsideDirectory(input.resolvedPath, input.workspace.rootDir)) {
261
206
  return;
262
207
  }
263
208
 
264
- const position = input.sourceFile.getLineAndCharacterOfPosition(
265
- input.node.getStart(input.sourceFile),
266
- );
267
209
  throw new Error(
268
- `${input.importer}:${position.line + 1}:${position.character + 1} ` +
210
+ `${input.importer}:${input.line}:${input.column} ` +
269
211
  `Local play imports must stay inside the play workspace (${input.workspace.rootDir}). ` +
270
212
  `Import "${input.specifier}" resolved to ${input.resolvedPath}, which crosses into app/backend code. ` +
271
213
  'Use the public SDK/API surface or move shared helpers into the play workspace.',
@@ -280,95 +222,156 @@ function getPackageName(specifier: string): string {
280
222
  return specifier.split('/')[0] ?? specifier;
281
223
  }
282
224
 
283
- function scriptKindForFile(filePath: string): ts.ScriptKind {
284
- const extension = extname(filePath).toLowerCase();
285
- switch (extension) {
286
- case '.tsx':
287
- return ts.ScriptKind.TSX;
288
- case '.jsx':
289
- return ts.ScriptKind.JSX;
290
- case '.js':
291
- case '.mjs':
292
- case '.cjs':
293
- return ts.ScriptKind.JS;
294
- case '.json':
295
- return ts.ScriptKind.JSON;
296
- default:
297
- return ts.ScriptKind.TS;
225
+ function isPlaySourceFile(filePath: string): boolean {
226
+ return PLAY_SOURCE_FILE_PATTERN.test(filePath);
227
+ }
228
+
229
+ function stripCommentsToSpaces(source: string): string {
230
+ return source
231
+ .replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, ' '))
232
+ .replace(/(^|[^:])\/\/.*$/gm, (match, prefix: string) =>
233
+ prefix + ' '.repeat(Math.max(0, match.length - prefix.length)),
234
+ );
235
+ }
236
+
237
+ function lineAndColumnAt(source: string, index: number): { line: number; column: number } {
238
+ const prefix = source.slice(0, index);
239
+ const lines = prefix.split('\n');
240
+ return { line: lines.length, column: lines[lines.length - 1]!.length + 1 };
241
+ }
242
+
243
+ function findSourceImportReferences(sourceCode: string): SourceImportReference[] {
244
+ const source = stripCommentsToSpaces(sourceCode);
245
+ const references: SourceImportReference[] = [];
246
+ const addReference = (
247
+ specifier: string | undefined,
248
+ specifierIndex: number,
249
+ kind: SourceImportReference['kind'],
250
+ ) => {
251
+ if (!specifier) return;
252
+ const position = lineAndColumnAt(sourceCode, specifierIndex);
253
+ references.push({
254
+ specifier,
255
+ line: position.line,
256
+ column: position.column,
257
+ kind,
258
+ });
259
+ };
260
+
261
+ const staticImportPattern =
262
+ /\b(?:import|export)\s+(?!type\b)(?:[\s\S]*?\s+from\s*)?(['"])([^'"\n]+)\1/g;
263
+ for (const match of source.matchAll(staticImportPattern)) {
264
+ addReference(match[2], match.index! + match[0].lastIndexOf(match[1]!), 'static');
265
+ }
266
+
267
+ const dynamicImportPattern = /\bimport\s*\(\s*(['"])([^'"\n]+)\1/g;
268
+ for (const match of source.matchAll(dynamicImportPattern)) {
269
+ addReference(match[2], match.index! + match[0].lastIndexOf(match[1]!), 'dynamic-import');
298
270
  }
271
+
272
+ const requirePattern = /\brequire\s*\(\s*(['"])([^'"\n]+)\1/g;
273
+ for (const match of source.matchAll(requirePattern)) {
274
+ addReference(match[2], match.index! + match[0].lastIndexOf(match[1]!), 'require');
275
+ }
276
+
277
+ const literalDynamicImportIndexes = new Set(
278
+ [...source.matchAll(dynamicImportPattern)].map((match) => match.index!),
279
+ );
280
+ for (const match of source.matchAll(/\bimport\s*\(/g)) {
281
+ if (literalDynamicImportIndexes.has(match.index!)) continue;
282
+ const position = lineAndColumnAt(sourceCode, match.index!);
283
+ throw new Error(
284
+ `:${position.line}:${position.column} Dynamic import() is not allowed in plays. Use static imports instead.`,
285
+ );
286
+ }
287
+
288
+ const literalRequireIndexes = new Set(
289
+ [...source.matchAll(requirePattern)].map((match) => match.index!),
290
+ );
291
+ for (const match of source.matchAll(/\brequire\s*\(/g)) {
292
+ if (literalRequireIndexes.has(match.index!)) continue;
293
+ const position = lineAndColumnAt(sourceCode, match.index!);
294
+ throw new Error(
295
+ `:${position.line}:${position.column} Dynamic require() is not allowed in plays. Use static imports or require("literal") only.`,
296
+ );
297
+ }
298
+
299
+ return references.sort((left, right) =>
300
+ left.line === right.line ? left.column - right.column : left.line - right.line,
301
+ );
299
302
  }
300
303
 
301
- function isPlaySourceFile(filePath: string): boolean {
302
- return PLAY_SOURCE_FILE_PATTERN.test(filePath);
304
+ function unquoteStringLiteral(literal: string): string | null {
305
+ const trimmed = literal.trim();
306
+ const quote = trimmed[0];
307
+ if ((quote !== '"' && quote !== "'") || trimmed[trimmed.length - 1] !== quote) {
308
+ return null;
309
+ }
310
+ try {
311
+ return JSON.parse(quote === '"' ? trimmed : `"${trimmed.slice(1, -1).replace(/"/g, '\\"')}"`);
312
+ } catch {
313
+ return trimmed.slice(1, -1);
314
+ }
303
315
  }
304
316
 
305
- function extractStringLiteralProperty(
306
- objectLiteral: ts.ObjectLiteralExpression,
307
- propertyName: string,
308
- ): string | null {
309
- for (const property of objectLiteral.properties) {
310
- if (!ts.isPropertyAssignment(property)) {
317
+ function findMatchingBrace(source: string, openIndex: number): number {
318
+ let depth = 0;
319
+ let quote: string | null = null;
320
+ let escaped = false;
321
+ for (let index = openIndex; index < source.length; index += 1) {
322
+ const char = source[index]!;
323
+ if (quote) {
324
+ if (escaped) {
325
+ escaped = false;
326
+ } else if (char === '\\') {
327
+ escaped = true;
328
+ } else if (char === quote) {
329
+ quote = null;
330
+ }
311
331
  continue;
312
332
  }
313
- const name = property.name;
314
- const matches =
315
- (ts.isIdentifier(name) && name.text === propertyName) ||
316
- (ts.isStringLiteralLike(name) && name.text === propertyName);
317
- if (!matches) {
333
+ if (char === '"' || char === "'" || char === '`') {
334
+ quote = char;
318
335
  continue;
319
336
  }
320
- return ts.isStringLiteralLike(property.initializer)
321
- ? property.initializer.text.trim()
322
- : null;
337
+ if (char === '{') depth += 1;
338
+ if (char === '}') {
339
+ depth -= 1;
340
+ if (depth === 0) return index;
341
+ }
323
342
  }
324
-
325
- return null;
343
+ return -1;
326
344
  }
327
345
 
328
346
  export function extractDefinedPlayName(
329
347
  sourceCode: string,
330
- filePath: string,
348
+ _filePath: string,
331
349
  ): string | null {
332
- const sourceFile = ts.createSourceFile(
333
- filePath,
334
- sourceCode,
335
- ts.ScriptTarget.Latest,
336
- true,
337
- scriptKindForFile(filePath),
338
- );
339
- let detectedPlayName: string | null = null;
340
-
341
- const visit = (node: ts.Node) => {
342
- if (detectedPlayName) {
343
- return;
350
+ const source = stripCommentsToSpaces(sourceCode);
351
+ const callPattern = /(?:\b[A-Za-z_$][\w$]*\s*\.\s*)?\b(?:definePlay|defineWorkflow)\s*\(/g;
352
+ for (const match of source.matchAll(callPattern)) {
353
+ const openParen = match.index! + match[0].length - 1;
354
+ const firstArgStart = openParen + 1;
355
+ const firstNonSpace = source.slice(firstArgStart).search(/\S/);
356
+ if (firstNonSpace < 0) continue;
357
+ const argIndex = firstArgStart + firstNonSpace;
358
+ const quote = source[argIndex];
359
+ if (quote === '"' || quote === "'") {
360
+ const literalMatch = source.slice(argIndex).match(/^(['"])(?:\\.|(?!\1)[\s\S])*\1/);
361
+ const value = literalMatch ? unquoteStringLiteral(literalMatch[0]) : null;
362
+ if (value?.trim()) return value.trim();
344
363
  }
345
-
346
- if (ts.isCallExpression(node)) {
347
- const expression = node.expression;
348
- const isDefinePlayCall =
349
- (ts.isIdentifier(expression) &&
350
- (expression.text === 'definePlay' || expression.text === 'defineWorkflow')) ||
351
- (ts.isPropertyAccessExpression(expression) &&
352
- (expression.name.text === 'definePlay' ||
353
- expression.name.text === 'defineWorkflow'));
354
- if (isDefinePlayCall) {
355
- const firstArgument = node.arguments[0];
356
- if (firstArgument && ts.isStringLiteralLike(firstArgument)) {
357
- detectedPlayName = firstArgument.text.trim() || null;
358
- return;
359
- }
360
- if (firstArgument && ts.isObjectLiteralExpression(firstArgument)) {
361
- detectedPlayName = extractStringLiteralProperty(firstArgument, 'id');
362
- return;
363
- }
364
+ if (quote === '{') {
365
+ const closeBrace = findMatchingBrace(source, argIndex);
366
+ if (closeBrace < 0) continue;
367
+ const objectSource = source.slice(argIndex + 1, closeBrace);
368
+ const idMatch = objectSource.match(/(?:^|[,{\s])(?:id|['"]id['"])\s*:\s*(['"])([\s\S]*?)\1/);
369
+ if (idMatch?.[2]?.trim()) {
370
+ return idMatch[2].trim();
364
371
  }
365
372
  }
366
-
367
- ts.forEachChild(node, visit);
368
- };
369
-
370
- visit(sourceFile);
371
- return detectedPlayName;
373
+ }
374
+ return null;
372
375
  }
373
376
 
374
377
  function getPackageRequireCandidates(fromFile: string) {
@@ -821,23 +824,15 @@ async function analyzeSourceGraph(
821
824
  return;
822
825
  }
823
826
 
824
- const sourceFile = ts.createSourceFile(
825
- absolutePath,
826
- sourceCode,
827
- ts.ScriptTarget.Latest,
828
- true,
829
- scriptKindForFile(absolutePath),
830
- );
831
-
832
827
  const handleSpecifier = async (
833
828
  specifier: string,
834
- node: ts.Node,
829
+ line: number,
830
+ column: number,
835
831
  kind: 'static' | 'require' | 'dynamic-import',
836
832
  ) => {
837
833
  if (kind === 'dynamic-import') {
838
- const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
839
834
  throw new Error(
840
- `${absolutePath}:${position.line + 1}:${position.character + 1} Dynamic import() is not allowed in plays. Use static imports instead.`,
835
+ `${absolutePath}:${line}:${column} Dynamic import() is not allowed in plays. Use static imports instead.`,
841
836
  );
842
837
  }
843
838
 
@@ -853,16 +848,15 @@ async function analyzeSourceGraph(
853
848
  specifier,
854
849
  resolvedPath: resolved,
855
850
  workspace,
856
- sourceFile,
857
- node,
851
+ line,
852
+ column,
858
853
  });
859
854
  if (resolved !== absoluteEntryFile && isPlaySourceFile(resolved)) {
860
855
  const importedSource = await readFile(resolved, 'utf-8');
861
856
  const importedPlayName = extractDefinedPlayName(importedSource, resolved);
862
857
  if (!importedPlayName) {
863
- const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
864
858
  throw new Error(
865
- `${absolutePath}:${position.line + 1}:${position.character + 1} Imported play file "${specifier}" must export definePlay(...) so it can be runtime-composed.`,
859
+ `${absolutePath}:${line}:${column} Imported play file "${specifier}" must export definePlay(...) so it can be runtime-composed.`,
866
860
  );
867
861
  }
868
862
  importedPlayDependencies.set(resolved, {
@@ -876,9 +870,8 @@ async function analyzeSourceGraph(
876
870
  }
877
871
 
878
872
  if (specifier.includes(':')) {
879
- const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
880
873
  throw new Error(
881
- `${absolutePath}:${position.line + 1}:${position.character + 1} Unsupported import specifier "${specifier}". Allowed imports are relative files, Node builtins, and installed packages.`,
874
+ `${absolutePath}:${line}:${column} Unsupported import specifier "${specifier}". Allowed imports are relative files, Node builtins, and installed packages.`,
882
875
  );
883
876
  }
884
877
 
@@ -886,48 +879,21 @@ async function analyzeSourceGraph(
886
879
  packages.set(packageImport.name, packageImport.version);
887
880
  };
888
881
 
889
- const walk = async (node: ts.Node): Promise<void> => {
890
- if (
891
- (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
892
- node.moduleSpecifier &&
893
- !(
894
- (ts.isImportDeclaration(node) && node.importClause?.isTypeOnly) ||
895
- (ts.isExportDeclaration(node) && node.isTypeOnly)
896
- ) &&
897
- ts.isStringLiteralLike(node.moduleSpecifier)
898
- ) {
899
- await handleSpecifier(node.moduleSpecifier.text, node, 'static');
900
- }
901
-
902
- if (ts.isCallExpression(node)) {
903
- if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
904
- if (node.arguments.length !== 1 || !ts.isStringLiteralLike(node.arguments[0]!)) {
905
- const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
906
- throw new Error(
907
- `${absolutePath}:${position.line + 1}:${position.character + 1} Dynamic import() is not allowed in plays. Use static imports instead.`,
908
- );
909
- }
910
- await handleSpecifier(node.arguments[0]!.text, node, 'dynamic-import');
911
- }
912
-
913
- if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
914
- const firstArgument = node.arguments[0];
915
- if (node.arguments.length !== 1 || !firstArgument || !ts.isStringLiteralLike(firstArgument)) {
916
- const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
917
- throw new Error(
918
- `${absolutePath}:${position.line + 1}:${position.character + 1} Dynamic require() is not allowed in plays. Use static imports or require(\"literal\") only.`,
919
- );
920
- }
921
- await handleSpecifier(firstArgument.text, node, 'require');
922
- }
882
+ try {
883
+ for (const reference of findSourceImportReferences(sourceCode)) {
884
+ await handleSpecifier(
885
+ reference.specifier,
886
+ reference.line,
887
+ reference.column,
888
+ reference.kind,
889
+ );
923
890
  }
924
-
925
- for (const child of node.getChildren(sourceFile)) {
926
- await walk(child);
891
+ } catch (error) {
892
+ if (error instanceof Error && error.message.startsWith(':')) {
893
+ throw new Error(`${absolutePath}${error.message}`);
927
894
  }
928
- };
929
-
930
- await walk(sourceFile);
895
+ throw error;
896
+ }
931
897
  };
932
898
 
933
899
  await visitFile(absoluteEntryFile);
@@ -956,6 +922,11 @@ async function analyzeSourceGraph(
956
922
 
957
923
  return {
958
924
  sourceCode,
925
+ sourceFiles: Object.fromEntries(
926
+ [...localFiles.entries()].sort((left, right) =>
927
+ left[0].localeCompare(right[0]),
928
+ ),
929
+ ),
959
930
  sourceHash,
960
931
  graphHash,
961
932
  importPolicy: {
@@ -1067,12 +1038,11 @@ function normalizeSourceMapForRuntime(sourceMapText: string): string {
1067
1038
  return JSON.stringify(parsed);
1068
1039
  }
1069
1040
 
1070
- function getBundleSizeError(
1041
+ export function getBundleSizeErrorForBytes(
1071
1042
  filePath: string,
1072
- bundledCode: string,
1043
+ bundleBytes: number,
1073
1044
  artifactKind: PlayArtifactKind,
1074
1045
  ): string | null {
1075
- const bundleBytes = Buffer.byteLength(bundledCode, 'utf8');
1076
1046
  if (bundleBytes > MAX_PLAY_BUNDLE_BYTES) {
1077
1047
  return `${filePath} Play bundle exceeds the 30 MiB limit (${bundleBytes} bytes > ${MAX_PLAY_BUNDLE_BYTES} bytes).`;
1078
1048
  }
@@ -1093,6 +1063,18 @@ function getBundleSizeError(
1093
1063
  return null;
1094
1064
  }
1095
1065
 
1066
+ function getBundleSizeError(
1067
+ filePath: string,
1068
+ bundledCode: string,
1069
+ artifactKind: PlayArtifactKind,
1070
+ ): string | null {
1071
+ return getBundleSizeErrorForBytes(
1072
+ filePath,
1073
+ Buffer.byteLength(bundledCode, 'utf8'),
1074
+ artifactKind,
1075
+ );
1076
+ }
1077
+
1096
1078
  export type BundlePlayFileOptions = {
1097
1079
  /**
1098
1080
  * Which artifact to produce. Defaults to `cjs_node20` (Daytona / local-process
@@ -1276,14 +1258,27 @@ export async function bundlePlayFile(
1276
1258
  `${analysis.graphHash}\nworkers-harness:${harnessFingerprint}`,
1277
1259
  );
1278
1260
  }
1279
- // Cache lookup before typecheck/discovery: a cached artifact at this
1280
- // graphHash is content-addressed by source — it can only exist if the
1281
- // exact same source already passed typecheckPlaySource on a previous
1282
- // run (we only write to cache after a successful bundle, which only
1283
- // runs after a successful typecheck). Re-running ts.createProgram +
1284
- // getPreEmitDiagnostics costs ~500ms per call and dominates warm
1285
- // bundle latency, so skipping it on a cache hit is the single biggest
1286
- // win for `play check` / `play run` startup time.
1261
+ const typecheckErrors = [
1262
+ ...((await adapter.typecheckPlaySource?.({
1263
+ sourceCode: analysis.sourceCode,
1264
+ sourcePath: absolutePath,
1265
+ importedFilePaths: [
1266
+ ...analysis.importPolicy.localFiles,
1267
+ ...analysis.importedPlayDependencies.map((dependency) => dependency.filePath),
1268
+ ],
1269
+ })) ?? []),
1270
+ ];
1271
+ if (typecheckErrors.length > 0) {
1272
+ return {
1273
+ success: false,
1274
+ filePath: absolutePath,
1275
+ errors: typecheckErrors,
1276
+ };
1277
+ }
1278
+
1279
+ // Cache lookup happens after validation because a bundle cache hit is keyed
1280
+ // by source and target, while cloud descriptor typecheck results also depend
1281
+ // on generated tool metadata.
1287
1282
  const cachedArtifact = await readArtifactCache(analysis.graphHash, target, adapter);
1288
1283
  const discoveredFiles = await adapter.discoverPackagedLocalFiles(absolutePath);
1289
1284
  if (cachedArtifact) {
@@ -1304,6 +1299,7 @@ export async function bundlePlayFile(
1304
1299
  success: true,
1305
1300
  artifact: { ...cachedArtifact, cacheHit: true },
1306
1301
  sourceCode: analysis.sourceCode,
1302
+ sourceFiles: analysis.sourceFiles,
1307
1303
  filePath: absolutePath,
1308
1304
  playName: analysis.playName,
1309
1305
  packagedFiles: discoveredFiles.files,
@@ -1312,26 +1308,6 @@ export async function bundlePlayFile(
1312
1308
  };
1313
1309
  }
1314
1310
 
1315
- const typecheckErrors = [
1316
- ...(adapter.typecheckSdkTypes === false
1317
- ? []
1318
- : typecheckPlaySource(analysis, adapter)),
1319
- ...((await adapter.typecheckPlaySource?.({
1320
- sourceCode: analysis.sourceCode,
1321
- sourcePath: absolutePath,
1322
- importedFilePaths: [
1323
- ...analysis.importPolicy.localFiles,
1324
- ...analysis.importedPlayDependencies.map((dependency) => dependency.filePath),
1325
- ],
1326
- })) ?? []),
1327
- ];
1328
- if (typecheckErrors.length > 0) {
1329
- return {
1330
- success: false,
1331
- filePath: absolutePath,
1332
- errors: typecheckErrors,
1333
- };
1334
- }
1335
1311
  const buildOutcome =
1336
1312
  target === PLAY_ARTIFACT_KINDS.esmWorkers
1337
1313
  ? await runEsbuildForEsmWorkers(absolutePath, analysis.importedPlayDependencies, adapter, exportName)
@@ -1391,6 +1367,7 @@ export async function bundlePlayFile(
1391
1367
  success: true,
1392
1368
  artifact,
1393
1369
  sourceCode: analysis.sourceCode,
1370
+ sourceFiles: analysis.sourceFiles,
1394
1371
  filePath: absolutePath,
1395
1372
  playName: analysis.playName,
1396
1373
  packagedFiles: discoveredFiles.files,
@@ -19,6 +19,21 @@ export type PlayDatasetBacking =
19
19
  file: PlayExecutionFileRef;
20
20
  };
21
21
 
22
+ export type PlayDatasetWorkProgressSummary = {
23
+ total: number;
24
+ executed: number;
25
+ reused: number;
26
+ skipped: number;
27
+ pending: number;
28
+ failed: number;
29
+ degraded?: boolean;
30
+ duplicates?: {
31
+ exact?: number;
32
+ semantic?: number;
33
+ rejected?: number;
34
+ };
35
+ };
36
+
22
37
  export interface SerializedPlayDataset<T> {
23
38
  kind: 'dataset';
24
39
  datasetKind: PlayDatasetKind;
@@ -28,6 +43,9 @@ export interface SerializedPlayDataset<T> {
28
43
  sourceLabel?: string | null;
29
44
  tableNamespace?: string | null;
30
45
  columns?: string[];
46
+ _metadata?: {
47
+ workProgress?: PlayDatasetWorkProgressSummary;
48
+ };
31
49
  preview: T[];
32
50
  }
33
51
 
@@ -61,6 +79,9 @@ export interface PlayDataset<T> extends AsyncIterable<T> {
61
79
  sourceLabel?: string | null;
62
80
  tableNamespace?: string | null;
63
81
  columns?: string[];
82
+ _metadata?: {
83
+ workProgress?: PlayDatasetWorkProgressSummary;
84
+ };
64
85
  preview: T[];
65
86
  };
66
87
  }
@@ -136,6 +157,7 @@ class DeferredPlayDataset<T> implements PlayDataset<T> {
136
157
  readonly tableNamespace?: string | null;
137
158
  private readonly previewRows: readonly T[];
138
159
  private readonly previewColumns?: string[];
160
+ private readonly workProgress?: PlayDatasetWorkProgressSummary;
139
161
  private cachedCount: number;
140
162
  private readonly resolvers: PlayDatasetResolvers<T>;
141
163
 
@@ -147,6 +169,7 @@ class DeferredPlayDataset<T> implements PlayDataset<T> {
147
169
  previewRows: readonly T[];
148
170
  sourceLabel?: string | null;
149
171
  tableNamespace?: string | null;
172
+ workProgress?: PlayDatasetWorkProgressSummary;
150
173
  resolvers: PlayDatasetResolvers<T>;
151
174
  }) {
152
175
  this.datasetKind = input.datasetKind;
@@ -157,6 +180,7 @@ class DeferredPlayDataset<T> implements PlayDataset<T> {
157
180
  this.previewColumns = inferPreviewColumns(this.previewRows);
158
181
  this.sourceLabel = input.sourceLabel ?? null;
159
182
  this.tableNamespace = input.tableNamespace ?? null;
183
+ this.workProgress = input.workProgress;
160
184
  this.resolvers = input.resolvers;
161
185
  }
162
186
 
@@ -212,6 +236,9 @@ class DeferredPlayDataset<T> implements PlayDataset<T> {
212
236
  ...(this.sourceLabel ? { sourceLabel: this.sourceLabel } : {}),
213
237
  ...(this.tableNamespace ? { tableNamespace: this.tableNamespace } : {}),
214
238
  ...(this.previewColumns ? { columns: this.previewColumns } : {}),
239
+ ...(this.workProgress
240
+ ? { _metadata: { workProgress: this.workProgress } }
241
+ : {}),
215
242
  preview: [...this.previewRows],
216
243
  };
217
244
  }
@@ -229,6 +256,7 @@ export function createDeferredPlayDataset<T>(input: {
229
256
  previewRows?: readonly T[];
230
257
  sourceLabel?: string | null;
231
258
  tableNamespace?: string | null;
259
+ workProgress?: PlayDatasetWorkProgressSummary;
232
260
  resolvers: PlayDatasetResolvers<T>;
233
261
  }): PlayDataset<T> {
234
262
  return new DeferredPlayDataset({