deepline 0.1.0 → 0.1.1
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/cli/index.js +212 -54
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +198 -40
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +3256 -0
- package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +710 -0
- package/dist/repo/apps/play-runner-workers/src/entry.ts +5070 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/README.md +21 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/batching.ts +177 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/execution-plan.ts +52 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/tool-batch.ts +100 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/tool-result.ts +184 -0
- package/dist/repo/sdk/src/cli/commands/auth.ts +482 -0
- package/dist/repo/sdk/src/cli/commands/billing.ts +188 -0
- package/dist/repo/sdk/src/cli/commands/csv.ts +123 -0
- package/dist/repo/sdk/src/cli/commands/db.ts +119 -0
- package/dist/repo/sdk/src/cli/commands/feedback.ts +40 -0
- package/dist/repo/sdk/src/cli/commands/org.ts +117 -0
- package/dist/repo/sdk/src/cli/commands/play.ts +3200 -0
- package/dist/repo/sdk/src/cli/commands/tools.ts +687 -0
- package/dist/repo/sdk/src/cli/dataset-stats.ts +341 -0
- package/dist/repo/sdk/src/cli/index.ts +138 -0
- package/dist/repo/sdk/src/cli/progress.ts +135 -0
- package/dist/repo/sdk/src/cli/trace.ts +61 -0
- package/dist/repo/sdk/src/cli/utils.ts +145 -0
- package/dist/repo/sdk/src/client.ts +1188 -0
- package/dist/repo/sdk/src/compat.ts +77 -0
- package/dist/repo/sdk/src/config.ts +285 -0
- package/dist/repo/sdk/src/errors.ts +125 -0
- package/dist/repo/sdk/src/http.ts +391 -0
- package/dist/repo/sdk/src/index.ts +139 -0
- package/dist/repo/sdk/src/play.ts +1330 -0
- package/dist/repo/sdk/src/plays/bundle-play-file.ts +133 -0
- package/dist/repo/sdk/src/plays/harness-stub.ts +210 -0
- package/dist/repo/sdk/src/plays/local-file-discovery.ts +326 -0
- package/dist/repo/sdk/src/tool-output.ts +489 -0
- package/dist/repo/sdk/src/types.ts +669 -0
- package/dist/repo/sdk/src/version.ts +2 -0
- package/dist/repo/sdk/src/worker-play-entry.ts +286 -0
- package/dist/repo/shared_libs/observability/node-tracing.ts +129 -0
- package/dist/repo/shared_libs/observability/tracing.ts +98 -0
- package/dist/repo/shared_libs/play-runtime/backend.ts +139 -0
- package/dist/repo/shared_libs/play-runtime/batch-runtime.ts +182 -0
- package/dist/repo/shared_libs/play-runtime/batching-types.ts +91 -0
- package/dist/repo/shared_libs/play-runtime/context.ts +3999 -0
- package/dist/repo/shared_libs/play-runtime/coordinator-headers.ts +78 -0
- package/dist/repo/shared_libs/play-runtime/ctx-contract.ts +250 -0
- package/dist/repo/shared_libs/play-runtime/ctx-types.ts +713 -0
- package/dist/repo/shared_libs/play-runtime/dataset-id.ts +10 -0
- package/dist/repo/shared_libs/play-runtime/db-session-crypto.ts +304 -0
- package/dist/repo/shared_libs/play-runtime/db-session.ts +462 -0
- package/dist/repo/shared_libs/play-runtime/dedup-backend.ts +0 -0
- package/dist/repo/shared_libs/play-runtime/default-batch-strategies.ts +124 -0
- package/dist/repo/shared_libs/play-runtime/execution-plan.ts +262 -0
- package/dist/repo/shared_libs/play-runtime/live-events.ts +214 -0
- package/dist/repo/shared_libs/play-runtime/live-state-contract.ts +50 -0
- package/dist/repo/shared_libs/play-runtime/map-execution-frame.ts +114 -0
- package/dist/repo/shared_libs/play-runtime/map-row-identity.ts +158 -0
- package/dist/repo/shared_libs/play-runtime/profiles.ts +90 -0
- package/dist/repo/shared_libs/play-runtime/progress-emitter.ts +172 -0
- package/dist/repo/shared_libs/play-runtime/protocol.ts +121 -0
- package/dist/repo/shared_libs/play-runtime/public-play-contract.ts +42 -0
- package/dist/repo/shared_libs/play-runtime/result-normalization.ts +33 -0
- package/dist/repo/shared_libs/play-runtime/runtime-actions.ts +208 -0
- package/dist/repo/shared_libs/play-runtime/runtime-api.ts +1873 -0
- package/dist/repo/shared_libs/play-runtime/runtime-constraints.ts +2 -0
- package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-neon-serverless.ts +201 -0
- package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-pg.ts +48 -0
- package/dist/repo/shared_libs/play-runtime/runtime-pg-driver.ts +84 -0
- package/dist/repo/shared_libs/play-runtime/scheduler-backend.ts +174 -0
- package/dist/repo/shared_libs/play-runtime/static-pipeline-types.ts +147 -0
- package/dist/repo/shared_libs/play-runtime/suspension.ts +68 -0
- package/dist/repo/shared_libs/play-runtime/tool-batch-executor.ts +146 -0
- package/dist/repo/shared_libs/play-runtime/tool-result.ts +387 -0
- package/dist/repo/shared_libs/play-runtime/tracing.ts +31 -0
- package/dist/repo/shared_libs/play-runtime/waterfall-replay.ts +75 -0
- package/dist/repo/shared_libs/play-runtime/worker-api-types.ts +140 -0
- package/dist/repo/shared_libs/plays/artifact-transport.ts +14 -0
- package/dist/repo/shared_libs/plays/artifact-types.ts +49 -0
- package/dist/repo/shared_libs/plays/bundling/index.ts +1346 -0
- package/dist/repo/shared_libs/plays/compiler-manifest.ts +186 -0
- package/dist/repo/shared_libs/plays/contracts.ts +51 -0
- package/dist/repo/shared_libs/plays/dataset.ts +308 -0
- package/dist/repo/shared_libs/plays/definition.ts +264 -0
- package/dist/repo/shared_libs/plays/file-refs.ts +11 -0
- package/dist/repo/shared_libs/plays/rate-limit-scheduler.ts +206 -0
- package/dist/repo/shared_libs/plays/resolve-static-pipeline.ts +164 -0
- package/dist/repo/shared_libs/plays/row-identity.ts +302 -0
- package/dist/repo/shared_libs/plays/runtime-validation.ts +415 -0
- package/dist/repo/shared_libs/plays/static-pipeline.ts +560 -0
- package/dist/repo/shared_libs/temporal/constants.ts +39 -0
- package/dist/repo/shared_libs/temporal/preview-config.ts +153 -0
- package/package.json +4 -4
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
export const POSTGRES_IDENTIFIER_MAX_LENGTH = 63;
|
|
2
|
+
export const PLAY_NAME_MAX_LENGTH = POSTGRES_IDENTIFIER_MAX_LENGTH;
|
|
3
|
+
export const MAP_KEY_NAMESPACE_MAX_LENGTH = POSTGRES_IDENTIFIER_MAX_LENGTH;
|
|
4
|
+
export const DEFAULT_TABLE_NAMESPACE = 'sheet';
|
|
5
|
+
export const DEFAULT_INTERNAL_TABLE_NAMESPACE = '_key';
|
|
6
|
+
|
|
7
|
+
const SHA256_INITIAL_HASH: number[] = [
|
|
8
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c,
|
|
9
|
+
0x1f83d9ab, 0x5be0cd19,
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const SHA256_ROUND_CONSTANTS: number[] = [
|
|
13
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
|
|
14
|
+
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
|
15
|
+
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
|
|
16
|
+
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
17
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
|
|
18
|
+
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
19
|
+
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
|
|
20
|
+
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
21
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
|
|
22
|
+
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
|
23
|
+
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function stableValue(value: unknown): unknown {
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return value.map((entry) => stableValue(entry));
|
|
29
|
+
}
|
|
30
|
+
if (value && typeof value === 'object') {
|
|
31
|
+
return Object.fromEntries(
|
|
32
|
+
Object.entries(value as Record<string, unknown>)
|
|
33
|
+
.filter(([, entry]) => entry !== undefined)
|
|
34
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
35
|
+
.map(([key, entry]) => [key, stableValue(entry)]),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function stableStringify(value: unknown): string {
|
|
42
|
+
return JSON.stringify(stableValue(value));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function rightRotate32(value: number, bits: number): number {
|
|
46
|
+
return (value >>> bits) | (value << (32 - bits));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sha256Hex(input: string): string {
|
|
50
|
+
const bytes = Array.from(new TextEncoder().encode(input));
|
|
51
|
+
const bitLength = bytes.length * 8;
|
|
52
|
+
bytes.push(0x80);
|
|
53
|
+
while (bytes.length % 64 !== 56) {
|
|
54
|
+
bytes.push(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const highBits = Math.floor(bitLength / 0x100000000);
|
|
58
|
+
const lowBits = bitLength >>> 0;
|
|
59
|
+
bytes.push(
|
|
60
|
+
(highBits >>> 24) & 0xff,
|
|
61
|
+
(highBits >>> 16) & 0xff,
|
|
62
|
+
(highBits >>> 8) & 0xff,
|
|
63
|
+
highBits & 0xff,
|
|
64
|
+
(lowBits >>> 24) & 0xff,
|
|
65
|
+
(lowBits >>> 16) & 0xff,
|
|
66
|
+
(lowBits >>> 8) & 0xff,
|
|
67
|
+
lowBits & 0xff,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const hash = [...SHA256_INITIAL_HASH];
|
|
71
|
+
const words = new Array<number>(64).fill(0);
|
|
72
|
+
|
|
73
|
+
for (let offset = 0; offset < bytes.length; offset += 64) {
|
|
74
|
+
for (let index = 0; index < 16; index += 1) {
|
|
75
|
+
const wordOffset = offset + index * 4;
|
|
76
|
+
words[index] =
|
|
77
|
+
((bytes[wordOffset] ?? 0) << 24) |
|
|
78
|
+
((bytes[wordOffset + 1] ?? 0) << 16) |
|
|
79
|
+
((bytes[wordOffset + 2] ?? 0) << 8) |
|
|
80
|
+
(bytes[wordOffset + 3] ?? 0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (let index = 16; index < 64; index += 1) {
|
|
84
|
+
const s0 =
|
|
85
|
+
rightRotate32(words[index - 15], 7) ^
|
|
86
|
+
rightRotate32(words[index - 15], 18) ^
|
|
87
|
+
(words[index - 15] >>> 3);
|
|
88
|
+
const s1 =
|
|
89
|
+
rightRotate32(words[index - 2], 17) ^
|
|
90
|
+
rightRotate32(words[index - 2], 19) ^
|
|
91
|
+
(words[index - 2] >>> 10);
|
|
92
|
+
words[index] = (words[index - 16] + s0 + words[index - 7] + s1) >>> 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let [a, b, c, d, e, f, g, h] = hash;
|
|
96
|
+
for (let index = 0; index < 64; index += 1) {
|
|
97
|
+
const s1 =
|
|
98
|
+
rightRotate32(e, 6) ^ rightRotate32(e, 11) ^ rightRotate32(e, 25);
|
|
99
|
+
const ch = (e & f) ^ (~e & g);
|
|
100
|
+
const temp1 =
|
|
101
|
+
(h + s1 + ch + SHA256_ROUND_CONSTANTS[index] + words[index]) >>> 0;
|
|
102
|
+
const s0 =
|
|
103
|
+
rightRotate32(a, 2) ^ rightRotate32(a, 13) ^ rightRotate32(a, 22);
|
|
104
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
105
|
+
const temp2 = (s0 + maj) >>> 0;
|
|
106
|
+
|
|
107
|
+
h = g;
|
|
108
|
+
g = f;
|
|
109
|
+
f = e;
|
|
110
|
+
e = (d + temp1) >>> 0;
|
|
111
|
+
d = c;
|
|
112
|
+
c = b;
|
|
113
|
+
b = a;
|
|
114
|
+
a = (temp1 + temp2) >>> 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
hash[0] = (hash[0] + a) >>> 0;
|
|
118
|
+
hash[1] = (hash[1] + b) >>> 0;
|
|
119
|
+
hash[2] = (hash[2] + c) >>> 0;
|
|
120
|
+
hash[3] = (hash[3] + d) >>> 0;
|
|
121
|
+
hash[4] = (hash[4] + e) >>> 0;
|
|
122
|
+
hash[5] = (hash[5] + f) >>> 0;
|
|
123
|
+
hash[6] = (hash[6] + g) >>> 0;
|
|
124
|
+
hash[7] = (hash[7] + h) >>> 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return hash.map((word) => word.toString(16).padStart(8, '0')).join('');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sanitizeIdentifierPart(value: string): string {
|
|
131
|
+
return value
|
|
132
|
+
.trim()
|
|
133
|
+
.replace(/[^a-z0-9]+/gi, '_')
|
|
134
|
+
.replace(/_+/g, '_')
|
|
135
|
+
.replace(/^_+|_+$/g, '')
|
|
136
|
+
.toLowerCase();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function validateIdentifierPart(
|
|
140
|
+
rawValue: string,
|
|
141
|
+
label: string,
|
|
142
|
+
maxLength: number,
|
|
143
|
+
): string {
|
|
144
|
+
const sanitized = sanitizeIdentifierPart(rawValue);
|
|
145
|
+
if (!sanitized) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`${label} must contain at least one letter or number after normalization. ` +
|
|
148
|
+
`Use only letters, numbers, underscores, or hyphens.`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (sanitized.length > maxLength) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`${label} is too long after normalization (${sanitized.length}/${maxLength}). ` +
|
|
155
|
+
`Shorten it to ${maxLength} characters or fewer. ` +
|
|
156
|
+
`Normalized value: "${sanitized}".`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return sanitized;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function normalizePlayName(value: string): string {
|
|
164
|
+
if (value.includes('/')) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
'Play name cannot contain "/". Slash is reserved for qualified play references like "prebuilt/example" or "self/example".',
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return validateIdentifierPart(value, 'Play name', PLAY_NAME_MAX_LENGTH);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function normalizePlayNameForSheet(value: string): string {
|
|
173
|
+
if (!value.includes('/')) {
|
|
174
|
+
return normalizePlayName(value);
|
|
175
|
+
}
|
|
176
|
+
const digest = sha256Hex(value).slice(0, 12);
|
|
177
|
+
const normalizedReference = sanitizeIdentifierPart(
|
|
178
|
+
value.replace(/\//g, '__'),
|
|
179
|
+
);
|
|
180
|
+
const prefixLength = Math.max(1, PLAY_NAME_MAX_LENGTH - digest.length - 1);
|
|
181
|
+
const prefix =
|
|
182
|
+
normalizedReference.slice(0, prefixLength).replace(/_+$/g, '') ||
|
|
183
|
+
'qualified_play';
|
|
184
|
+
return `${prefix}_${digest}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function normalizeTableNamespace(value: string): string {
|
|
188
|
+
return validateIdentifierPart(
|
|
189
|
+
value,
|
|
190
|
+
'ctx.map() key',
|
|
191
|
+
MAP_KEY_NAMESPACE_MAX_LENGTH,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function resolveStaleMapTableNamespace(
|
|
196
|
+
mapKey: string,
|
|
197
|
+
staleAfterSeconds?: number | null,
|
|
198
|
+
nowMs: number = Date.now(),
|
|
199
|
+
): string {
|
|
200
|
+
const normalizedMapKey = normalizeTableNamespace(mapKey);
|
|
201
|
+
if (staleAfterSeconds === undefined || staleAfterSeconds === null) {
|
|
202
|
+
return normalizedMapKey;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (
|
|
206
|
+
!Number.isFinite(staleAfterSeconds) ||
|
|
207
|
+
!Number.isInteger(staleAfterSeconds) ||
|
|
208
|
+
staleAfterSeconds <= 0
|
|
209
|
+
) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
'ctx.map() staleAfterSeconds must be a positive whole number of seconds.',
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const bucket = Math.floor(nowMs / (staleAfterSeconds * 1000));
|
|
216
|
+
const stalePartitionKey = `stale_${staleAfterSeconds}_${bucket}`;
|
|
217
|
+
const candidate = `${normalizedMapKey}_${stalePartitionKey}`;
|
|
218
|
+
if (candidate.length <= MAP_KEY_NAMESPACE_MAX_LENGTH) {
|
|
219
|
+
return candidate;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const digest = sha256Hex(`${normalizedMapKey}\n${stalePartitionKey}`).slice(
|
|
223
|
+
0,
|
|
224
|
+
12,
|
|
225
|
+
);
|
|
226
|
+
const prefixLength = Math.max(
|
|
227
|
+
1,
|
|
228
|
+
MAP_KEY_NAMESPACE_MAX_LENGTH - digest.length - 1,
|
|
229
|
+
);
|
|
230
|
+
const prefix =
|
|
231
|
+
normalizedMapKey.slice(0, prefixLength).replace(/_+$/g, '') || 'map';
|
|
232
|
+
return `${prefix}_${digest}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function maxArtifactTableNamespaceLength(
|
|
236
|
+
playName?: string | null,
|
|
237
|
+
): number {
|
|
238
|
+
if (!playName?.trim()) {
|
|
239
|
+
return POSTGRES_IDENTIFIER_MAX_LENGTH;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const normalizedPlayName = normalizePlayNameForSheet(playName);
|
|
243
|
+
return Math.max(
|
|
244
|
+
1,
|
|
245
|
+
POSTGRES_IDENTIFIER_MAX_LENGTH - normalizedPlayName.length - 1,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function validatePlaySheetTableName(
|
|
250
|
+
playName: string,
|
|
251
|
+
tableNamespace: string,
|
|
252
|
+
): string {
|
|
253
|
+
const playSegment = normalizePlayNameForSheet(playName);
|
|
254
|
+
const keySegment = normalizeTableNamespace(tableNamespace);
|
|
255
|
+
const resolved = `${playSegment}_${keySegment}`;
|
|
256
|
+
if (resolved.length > POSTGRES_IDENTIFIER_MAX_LENGTH) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`Play sheet table name is too long after normalization (${resolved.length}/63). ` +
|
|
259
|
+
`Shorten the play name or ctx.map() key. ` +
|
|
260
|
+
`Resolved table name: "${resolved}".`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return resolved;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function resolvePlayRunTableNamespace(
|
|
267
|
+
tableNamespace: string,
|
|
268
|
+
_input?: Record<string, unknown> | null,
|
|
269
|
+
_options?: {
|
|
270
|
+
playName?: string | null;
|
|
271
|
+
},
|
|
272
|
+
): string {
|
|
273
|
+
return normalizeTableNamespace(tableNamespace);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function derivePlayRowIdentity(
|
|
277
|
+
row: Record<string, unknown>,
|
|
278
|
+
tableNamespace: string,
|
|
279
|
+
): string {
|
|
280
|
+
const normalizedNamespace = normalizeTableNamespace(tableNamespace);
|
|
281
|
+
const canonicalRow = stableStringify(row);
|
|
282
|
+
const digest = sha256Hex(`${normalizedNamespace}\n${canonicalRow}`);
|
|
283
|
+
return `${normalizedNamespace}:${digest}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Build a stable row identity from an explicit user-provided key string.
|
|
288
|
+
*
|
|
289
|
+
* Use when the caller wants to pin row identity to a primary column
|
|
290
|
+
* (e.g. row.email) instead of hashing the full row contents — so harmless
|
|
291
|
+
* mutations to other columns don't invalidate the cache.
|
|
292
|
+
*/
|
|
293
|
+
export function derivePlayRowIdentityFromKey(
|
|
294
|
+
key: string,
|
|
295
|
+
tableNamespace: string,
|
|
296
|
+
): string {
|
|
297
|
+
const normalizedNamespace = normalizeTableNamespace(tableNamespace);
|
|
298
|
+
const digest = sha256Hex(`${normalizedNamespace}\nkey:${key}`);
|
|
299
|
+
return `${normalizedNamespace}:${digest}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export const normalizeMapKeyNamespace = normalizeTableNamespace;
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import * as acorn from 'acorn';
|
|
2
|
+
import { fullAncestor as walkFullAncestor } from 'acorn-walk';
|
|
3
|
+
import type { PlayBundleArtifact } from './artifact-types';
|
|
4
|
+
import type { PlayStructuredDefinition } from './definition';
|
|
5
|
+
import { validatePlayStructuredDefinition } from './definition';
|
|
6
|
+
import {
|
|
7
|
+
MAP_KEY_NAMESPACE_MAX_LENGTH,
|
|
8
|
+
normalizeTableNamespace,
|
|
9
|
+
} from './row-identity';
|
|
10
|
+
import { DISALLOWED_RUN_JAVASCRIPT_TOOL_MESSAGE } from '../play-runtime/runtime-constraints';
|
|
11
|
+
|
|
12
|
+
export async function validatePlay(
|
|
13
|
+
code: string | null | undefined,
|
|
14
|
+
definition?: PlayStructuredDefinition | null,
|
|
15
|
+
codeFormat: 'function' | 'cjs_module' | 'esm_module' = 'function',
|
|
16
|
+
validationSource?: string | null,
|
|
17
|
+
artifact?: PlayBundleArtifact | null,
|
|
18
|
+
): Promise<{ valid: boolean; errors: string[] }> {
|
|
19
|
+
const errors: string[] = [];
|
|
20
|
+
|
|
21
|
+
if (definition) {
|
|
22
|
+
return validatePlayStructuredDefinition(definition);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const sourceForValidation = validationSource ?? code ?? '';
|
|
26
|
+
|
|
27
|
+
if (!sourceForValidation.trim()) {
|
|
28
|
+
return { valid: false, errors: ['Play code is required.'] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
if (codeFormat === 'cjs_module' && code?.trim()) {
|
|
33
|
+
new Function('module', 'exports', 'require', code);
|
|
34
|
+
} else if (codeFormat === 'esm_module' && code?.trim()) {
|
|
35
|
+
// esm_module bundles target Cloudflare Workers; their top-level uses
|
|
36
|
+
// import/export which `new Function` rejects. Skip the in-host parse
|
|
37
|
+
// check — the bundler already typechecked + esbuild parsed the source.
|
|
38
|
+
// The play Worker runtime catches any actual runtime errors.
|
|
39
|
+
} else if (codeFormat !== 'cjs_module') {
|
|
40
|
+
new Function('ctx', 'input', `return (${code})(ctx, input)`);
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {
|
|
43
|
+
errors.push(`Parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
errors.push(...validatePlayMapStructure(sourceForValidation));
|
|
47
|
+
errors.push(...validateRuntimeSyntax(sourceForValidation));
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
artifact &&
|
|
51
|
+
artifact.codeFormat !== 'cjs_module' &&
|
|
52
|
+
artifact.codeFormat !== 'esm_module'
|
|
53
|
+
) {
|
|
54
|
+
errors.push(
|
|
55
|
+
'Play artifact codeFormat must be "cjs_module" or "esm_module".',
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { valid: errors.length === 0, errors };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function validatePlayMapStructure(code: string): string[] {
|
|
63
|
+
const errors: string[] = [];
|
|
64
|
+
const calledPlayNames = new Set<string>();
|
|
65
|
+
let ast: acorn.Node;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
ast = parsePlayAst(code);
|
|
69
|
+
} catch {
|
|
70
|
+
return errors;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
walkFullAncestor(ast, (node, _state, ancestors) => {
|
|
74
|
+
if (
|
|
75
|
+
node.type === 'CallExpression' &&
|
|
76
|
+
usesDisallowedRunJavascriptTool(node as acorn.CallExpression) &&
|
|
77
|
+
hasMapResolverAncestor(ancestors)
|
|
78
|
+
) {
|
|
79
|
+
errors.push(DISALLOWED_RUN_JAVASCRIPT_TOOL_MESSAGE);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!isCtxMapCall(node)) {
|
|
83
|
+
const callExpression =
|
|
84
|
+
(node as acorn.Node).type === 'CallExpression'
|
|
85
|
+
? (node as unknown as acorn.CallExpression)
|
|
86
|
+
: null;
|
|
87
|
+
if (
|
|
88
|
+
callExpression &&
|
|
89
|
+
callExpression.callee.type === 'MemberExpression' &&
|
|
90
|
+
callExpression.callee.property.type === 'Identifier' &&
|
|
91
|
+
callExpression.callee.property.name === 'runPlay'
|
|
92
|
+
) {
|
|
93
|
+
const firstArgument = callExpression.arguments[0];
|
|
94
|
+
if (
|
|
95
|
+
firstArgument?.type === 'Literal' &&
|
|
96
|
+
typeof firstArgument.value === 'string'
|
|
97
|
+
) {
|
|
98
|
+
calledPlayNames.add(firstArgument.value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
callExpression &&
|
|
103
|
+
callExpression.callee.type === 'MemberExpression' &&
|
|
104
|
+
callExpression.callee.property.type === 'Identifier' &&
|
|
105
|
+
callExpression.callee.property.name === 'waterfall'
|
|
106
|
+
) {
|
|
107
|
+
const firstArgument = callExpression.arguments[0];
|
|
108
|
+
if (firstArgument?.type === 'ObjectExpression') {
|
|
109
|
+
const minResultsProperty = firstArgument.properties.find(
|
|
110
|
+
(prop) =>
|
|
111
|
+
prop.type === 'Property' &&
|
|
112
|
+
prop.key.type === 'Identifier' &&
|
|
113
|
+
prop.key.name === 'minResults',
|
|
114
|
+
);
|
|
115
|
+
if (!minResultsProperty) {
|
|
116
|
+
errors.push(
|
|
117
|
+
'Inline ctx.waterfall({...}) calls must declare minResults.',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (hasMapResolverAncestor(ancestors)) {
|
|
126
|
+
errors.push(
|
|
127
|
+
'Nested ctx.map() is not supported. Flatten work into a single map definition.',
|
|
128
|
+
);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
extractValidatedMapTableNamespace(node, errors);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const playNameMatch = code.match(
|
|
136
|
+
/define(?:Play|Workflow)\s*\(\s*['"`]([^'"`]+)['"`]/,
|
|
137
|
+
);
|
|
138
|
+
const definedPlayName = playNameMatch?.[1]?.trim();
|
|
139
|
+
if (definedPlayName && calledPlayNames.has(definedPlayName)) {
|
|
140
|
+
errors.push(
|
|
141
|
+
`Recursive play graph detected: ${definedPlayName} -> ${definedPlayName}. Use a different child play or refactor the shared logic.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return [...new Set(errors)];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function validateRuntimeSyntax(code: string): string[] {
|
|
149
|
+
const errors: string[] = [];
|
|
150
|
+
let ast: acorn.Node;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
ast = parsePlayAst(code);
|
|
154
|
+
} catch {
|
|
155
|
+
return errors;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
walkFullAncestor(ast, (node) => {
|
|
159
|
+
if (node.type === 'ImportExpression') {
|
|
160
|
+
errors.push(
|
|
161
|
+
'Dynamic import() is not allowed in plays. Use static imports instead.',
|
|
162
|
+
);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (node.type !== 'CallExpression') {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const callNode = node as acorn.CallExpression;
|
|
171
|
+
if (
|
|
172
|
+
callNode.callee.type !== 'Identifier' ||
|
|
173
|
+
callNode.callee.name !== 'require'
|
|
174
|
+
) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const firstArgument = callNode.arguments[0];
|
|
179
|
+
const isLiteralString =
|
|
180
|
+
firstArgument?.type === 'Literal' &&
|
|
181
|
+
typeof firstArgument.value === 'string';
|
|
182
|
+
|
|
183
|
+
if (!isLiteralString) {
|
|
184
|
+
errors.push(
|
|
185
|
+
'Dynamic require() is not allowed in plays. Use require("literal") only.',
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return [...new Set(errors)];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parsePlayAst(code: string): acorn.Node {
|
|
194
|
+
try {
|
|
195
|
+
return acorn.parse(code, {
|
|
196
|
+
ecmaVersion: 'latest',
|
|
197
|
+
sourceType: 'module',
|
|
198
|
+
allowAwaitOutsideFunction: true,
|
|
199
|
+
}) as acorn.Node;
|
|
200
|
+
} catch {
|
|
201
|
+
return acorn.parse(`const __play = ${code};`, {
|
|
202
|
+
ecmaVersion: 'latest',
|
|
203
|
+
sourceType: 'module',
|
|
204
|
+
}) as acorn.Node;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function usesDisallowedRunJavascriptTool(node: acorn.CallExpression): boolean {
|
|
209
|
+
const callee = node.callee;
|
|
210
|
+
if (callee.type !== 'MemberExpression') {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (
|
|
215
|
+
callee.property.type !== 'Identifier' ||
|
|
216
|
+
callee.property.name !== 'tool'
|
|
217
|
+
) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const firstArgument = node.arguments[0];
|
|
222
|
+
return (
|
|
223
|
+
firstArgument?.type === 'Literal' &&
|
|
224
|
+
firstArgument.value === 'run_javascript'
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function extractValidatedMapTableNamespace(
|
|
229
|
+
node: acorn.CallExpression,
|
|
230
|
+
errors: string[],
|
|
231
|
+
): string | null {
|
|
232
|
+
const keyArgument = node.arguments[0];
|
|
233
|
+
const rowsArgument = node.arguments[1];
|
|
234
|
+
if (!keyArgument) {
|
|
235
|
+
errors.push(
|
|
236
|
+
'ctx.map() requires a string literal map key as the first argument, e.g. ctx.map("leads", rows, { key: "lead_id" }).step("company", row => row.domain).run(...).',
|
|
237
|
+
);
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!rowsArgument) {
|
|
242
|
+
errors.push(
|
|
243
|
+
'ctx.map() requires rows as the second argument, e.g. ctx.map("leads", rows, { key: "lead_id" }).step("company", row => row.domain).run(...).',
|
|
244
|
+
);
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (keyArgument.type !== 'Literal' || typeof keyArgument.value !== 'string') {
|
|
249
|
+
errors.push(
|
|
250
|
+
'ctx.map() requires a string literal key as the first argument so Deepline can precompute idempotency.',
|
|
251
|
+
);
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!keyArgument.value.trim()) {
|
|
256
|
+
errors.push(
|
|
257
|
+
'ctx.map() requires a non-empty string key as the first argument.',
|
|
258
|
+
);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
normalizeTableNamespace(keyArgument.value);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
errors.push(
|
|
266
|
+
error instanceof Error
|
|
267
|
+
? `${error.message} Example: ctx.map("leads", rows, { key: "lead_id" }).step("company", row => row.domain).run({ description: "..." }).`
|
|
268
|
+
: `ctx.map() key must normalize to <= ${MAP_KEY_NAMESPACE_MAX_LENGTH} characters.`,
|
|
269
|
+
);
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (rowsArgument.type === 'ObjectExpression') {
|
|
274
|
+
errors.push(
|
|
275
|
+
'ctx.map() key must not be an object. Use ctx.map("leads", rows, { key: "lead_id" }).step(...).run(...).',
|
|
276
|
+
);
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
if (
|
|
280
|
+
rowsArgument.type === 'FunctionExpression' ||
|
|
281
|
+
isFunctionNode(rowsArgument)
|
|
282
|
+
) {
|
|
283
|
+
errors.push('ctx.map() requires rows as the second argument.');
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const optionsArgument = node.arguments[2];
|
|
288
|
+
if (optionsArgument && !isMapDefinitionOptionsNode(optionsArgument)) {
|
|
289
|
+
errors.push('ctx.map() accepts key, rows, and map options only.');
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return keyArgument.value.trim();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function isMapDefinitionOptionsNode(node: acorn.Node): boolean {
|
|
297
|
+
if (node.type !== 'ObjectExpression') return false;
|
|
298
|
+
const properties = (
|
|
299
|
+
node as acorn.Node & {
|
|
300
|
+
properties: acorn.Node[];
|
|
301
|
+
}
|
|
302
|
+
).properties;
|
|
303
|
+
return properties.every((property) => {
|
|
304
|
+
if (property.type !== 'Property') return false;
|
|
305
|
+
const key = (property as acorn.Node & { key: acorn.Node }).key as
|
|
306
|
+
| (acorn.Node & { type: 'Identifier'; name: string })
|
|
307
|
+
| (acorn.Node & { type: 'Literal'; value: unknown });
|
|
308
|
+
const name =
|
|
309
|
+
key.type === 'Identifier'
|
|
310
|
+
? key.name
|
|
311
|
+
: key.type === 'Literal' && typeof key.value === 'string'
|
|
312
|
+
? key.value
|
|
313
|
+
: null;
|
|
314
|
+
return name === 'key' || name === 'staleAfterSeconds';
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function isCtxMapCall(node: acorn.Node): node is acorn.CallExpression {
|
|
319
|
+
if (node.type !== 'CallExpression') {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
const callee = (node as acorn.CallExpression).callee;
|
|
323
|
+
if (callee.type !== 'MemberExpression') {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
if (callee.property.type !== 'Identifier' || callee.property.name !== 'map') {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
if (callee.object.type !== 'Identifier') {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
return callee.object.name === 'ctx' || callee.object.name.endsWith('Ctx');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function isFunctionNode(
|
|
336
|
+
node: acorn.Node | null | undefined,
|
|
337
|
+
): node is
|
|
338
|
+
| acorn.FunctionDeclaration
|
|
339
|
+
| acorn.FunctionExpression
|
|
340
|
+
| acorn.ArrowFunctionExpression {
|
|
341
|
+
return Boolean(
|
|
342
|
+
node &&
|
|
343
|
+
(node.type === 'FunctionDeclaration' ||
|
|
344
|
+
node.type === 'FunctionExpression' ||
|
|
345
|
+
node.type === 'ArrowFunctionExpression'),
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function hasMapResolverAncestor(ancestors: acorn.Node[]): boolean {
|
|
350
|
+
for (let index = 0; index < ancestors.length; index += 1) {
|
|
351
|
+
const node = ancestors[index];
|
|
352
|
+
if (!isFunctionNode(node)) {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const parent = index >= 1 ? ancestors[index - 1] : null;
|
|
357
|
+
const grandparent = index >= 2 ? ancestors[index - 2] : null;
|
|
358
|
+
const greatGrandparent = index >= 3 ? ancestors[index - 3] : null;
|
|
359
|
+
|
|
360
|
+
if (
|
|
361
|
+
parent?.type === 'CallExpression' &&
|
|
362
|
+
isCtxMapCall(parent) &&
|
|
363
|
+
parent.arguments[2] === node
|
|
364
|
+
) {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (
|
|
369
|
+
parent?.type === 'CallExpression' &&
|
|
370
|
+
isMapBuilderStepCall(parent) &&
|
|
371
|
+
parent.arguments[1] === node
|
|
372
|
+
) {
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (
|
|
377
|
+
parent?.type === 'Property' &&
|
|
378
|
+
(parent as acorn.Property).value === node &&
|
|
379
|
+
grandparent?.type === 'ObjectExpression' &&
|
|
380
|
+
greatGrandparent?.type === 'CallExpression' &&
|
|
381
|
+
isCtxMapCall(greatGrandparent) &&
|
|
382
|
+
greatGrandparent.arguments[2] === grandparent
|
|
383
|
+
) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function isMapBuilderStepCall(node: acorn.Node): node is acorn.CallExpression {
|
|
392
|
+
if (node.type !== 'CallExpression') return false;
|
|
393
|
+
const callee = (node as acorn.CallExpression).callee;
|
|
394
|
+
if (callee.type !== 'MemberExpression') return false;
|
|
395
|
+
if (
|
|
396
|
+
callee.property.type !== 'Identifier' ||
|
|
397
|
+
callee.property.name !== 'step'
|
|
398
|
+
) {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
let current: acorn.Node = callee.object as acorn.Node;
|
|
402
|
+
while (current.type === 'CallExpression') {
|
|
403
|
+
if (isCtxMapCall(current)) return true;
|
|
404
|
+
const nestedCallee = (current as acorn.CallExpression).callee;
|
|
405
|
+
if (
|
|
406
|
+
nestedCallee.type !== 'MemberExpression' ||
|
|
407
|
+
nestedCallee.property.type !== 'Identifier' ||
|
|
408
|
+
nestedCallee.property.name !== 'step'
|
|
409
|
+
) {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
current = nestedCallee.object as acorn.Node;
|
|
413
|
+
}
|
|
414
|
+
return false;
|
|
415
|
+
}
|