deepline 0.1.74 → 0.1.77
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 +1305 -345
- package/dist/cli/index.mjs +1307 -347
- package/dist/index.d.mts +77 -1
- package/dist/index.d.ts +77 -1
- package/dist/index.js +201 -3
- package/dist/index.mjs +201 -3
- package/dist/repo/apps/play-runner-workers/src/entry.ts +101 -45
- package/dist/repo/sdk/src/client.ts +8 -0
- package/dist/repo/sdk/src/play.ts +2 -1
- package/dist/repo/sdk/src/plays/harness-stub.ts +1 -0
- package/dist/repo/sdk/src/release.ts +3 -3
- package/dist/repo/sdk/src/worker-play-entry.ts +3 -0
- package/dist/repo/shared_libs/play-runtime/cell-staleness.ts +88 -0
- package/dist/repo/shared_libs/play-runtime/email-status.ts +301 -0
- package/dist/repo/shared_libs/play-runtime/step-program-dataset-builder.ts +5 -0
- package/dist/repo/shared_libs/play-runtime/tool-result.ts +58 -1
- package/dist/repo/shared_libs/plays/row-identity.ts +0 -40
- package/package.json +1 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export type CellStalenessPolicy = {
|
|
2
|
+
staleAfterSeconds?: number;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export type CellStalenessMeta = {
|
|
6
|
+
status?: string | null;
|
|
7
|
+
completedAt?: number | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type CellStalenessDecision =
|
|
11
|
+
| { action: 'recompute'; reason: 'missing' | 'failed' | 'stale' }
|
|
12
|
+
| { action: 'reuse'; reason: 'fresh' | 'no_policy' };
|
|
13
|
+
|
|
14
|
+
export type CellStalenessPolicyByField = Record<string, CellStalenessPolicy>;
|
|
15
|
+
|
|
16
|
+
export const DEEPLINE_CELL_META_FIELD = '__deeplineCellMeta';
|
|
17
|
+
|
|
18
|
+
export function validateStaleAfterSeconds(
|
|
19
|
+
staleAfterSeconds: number | undefined,
|
|
20
|
+
label = 'staleAfterSeconds',
|
|
21
|
+
): void {
|
|
22
|
+
if (staleAfterSeconds === undefined) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (
|
|
26
|
+
!Number.isFinite(staleAfterSeconds) ||
|
|
27
|
+
!Number.isInteger(staleAfterSeconds) ||
|
|
28
|
+
staleAfterSeconds <= 0
|
|
29
|
+
) {
|
|
30
|
+
throw new Error(`${label} must be a positive whole number of seconds.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function normalizeCellStalenessPolicy(
|
|
35
|
+
policy: CellStalenessPolicy | undefined,
|
|
36
|
+
): CellStalenessPolicy {
|
|
37
|
+
validateStaleAfterSeconds(policy?.staleAfterSeconds);
|
|
38
|
+
return policy?.staleAfterSeconds === undefined
|
|
39
|
+
? {}
|
|
40
|
+
: { staleAfterSeconds: policy.staleAfterSeconds };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function shouldRecomputeCell(input: {
|
|
44
|
+
hasValue: boolean;
|
|
45
|
+
meta?: CellStalenessMeta | null;
|
|
46
|
+
policy?: CellStalenessPolicy;
|
|
47
|
+
nowMs?: number;
|
|
48
|
+
}): CellStalenessDecision {
|
|
49
|
+
if (!input.hasValue) {
|
|
50
|
+
return { action: 'recompute', reason: 'missing' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const status = String(input.meta?.status ?? '').trim();
|
|
54
|
+
if (status === 'failed') {
|
|
55
|
+
return { action: 'recompute', reason: 'failed' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const staleAfterSeconds = input.policy?.staleAfterSeconds;
|
|
59
|
+
validateStaleAfterSeconds(staleAfterSeconds);
|
|
60
|
+
if (staleAfterSeconds === undefined) {
|
|
61
|
+
return { action: 'reuse', reason: 'no_policy' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const completedAt =
|
|
65
|
+
typeof input.meta?.completedAt === 'number' &&
|
|
66
|
+
Number.isFinite(input.meta.completedAt)
|
|
67
|
+
? input.meta.completedAt
|
|
68
|
+
: null;
|
|
69
|
+
if (completedAt === null) {
|
|
70
|
+
return { action: 'recompute', reason: 'missing' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const ageMs = Math.max(0, (input.nowMs ?? Date.now()) - completedAt);
|
|
74
|
+
return ageMs > staleAfterSeconds * 1000
|
|
75
|
+
? { action: 'recompute', reason: 'stale' }
|
|
76
|
+
: { action: 'reuse', reason: 'fresh' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function cellPolicyFields(
|
|
80
|
+
policies: CellStalenessPolicyByField | undefined,
|
|
81
|
+
): string[] {
|
|
82
|
+
if (!policies) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
return Object.entries(policies)
|
|
86
|
+
.filter(([, policy]) => policy.staleAfterSeconds !== undefined)
|
|
87
|
+
.map(([field]) => field);
|
|
88
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
export type EmailStatusVerdict =
|
|
2
|
+
| 'send'
|
|
3
|
+
| 'send_with_caution'
|
|
4
|
+
| 'verify_next'
|
|
5
|
+
| 'hold'
|
|
6
|
+
| 'drop';
|
|
7
|
+
|
|
8
|
+
export type EmailStatusValue =
|
|
9
|
+
| 'valid'
|
|
10
|
+
| 'invalid'
|
|
11
|
+
| 'catch_all'
|
|
12
|
+
| 'valid_catch_all'
|
|
13
|
+
| 'unknown'
|
|
14
|
+
| 'do_not_mail'
|
|
15
|
+
| 'spamtrap'
|
|
16
|
+
| 'abuse'
|
|
17
|
+
| 'disposable';
|
|
18
|
+
|
|
19
|
+
export type EmailDeliverability = 'high' | 'medium' | 'low' | 'unknown';
|
|
20
|
+
|
|
21
|
+
export type EmailMxClass =
|
|
22
|
+
| 'consumer_mailbox'
|
|
23
|
+
| 'workspace_mailbox'
|
|
24
|
+
| 'security_gateway'
|
|
25
|
+
| 'on_prem'
|
|
26
|
+
| 'unknown';
|
|
27
|
+
|
|
28
|
+
export type EmailStatus = {
|
|
29
|
+
verdict: EmailStatusVerdict;
|
|
30
|
+
status: EmailStatusValue;
|
|
31
|
+
verified: boolean;
|
|
32
|
+
confidence: number | null;
|
|
33
|
+
reasons: string[];
|
|
34
|
+
signals: {
|
|
35
|
+
catch_all: boolean | null;
|
|
36
|
+
deliverability: EmailDeliverability;
|
|
37
|
+
mx_class: EmailMxClass;
|
|
38
|
+
mx_provider: string | null;
|
|
39
|
+
mx_record: string | null;
|
|
40
|
+
fraud_score: number | null;
|
|
41
|
+
disposable: boolean | null;
|
|
42
|
+
role_based: boolean | null;
|
|
43
|
+
free_email: boolean | null;
|
|
44
|
+
abuse: boolean | null;
|
|
45
|
+
spamtrap: boolean | null;
|
|
46
|
+
suspect: boolean | null;
|
|
47
|
+
valid: boolean | null;
|
|
48
|
+
};
|
|
49
|
+
provider: {
|
|
50
|
+
name: string;
|
|
51
|
+
raw_status: string | boolean | number | null;
|
|
52
|
+
raw_score: number | null;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type EmailStatusMapEntry = {
|
|
57
|
+
status: EmailStatusValue;
|
|
58
|
+
verdict?: EmailStatusVerdict;
|
|
59
|
+
verified?: boolean;
|
|
60
|
+
reason?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type EmailStatusRule = EmailStatusMapEntry & {
|
|
64
|
+
when: Record<string, string | number | boolean | null>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type EmailStatusExtractorConfig = {
|
|
68
|
+
provider: string;
|
|
69
|
+
rawStatus?: string[];
|
|
70
|
+
rawScore?: string[];
|
|
71
|
+
valid?: string[];
|
|
72
|
+
deliverability?: string[];
|
|
73
|
+
catchAll?: string[];
|
|
74
|
+
mxProvider?: string[];
|
|
75
|
+
mxRecord?: string[];
|
|
76
|
+
fraudScore?: string[];
|
|
77
|
+
disposable?: string[];
|
|
78
|
+
roleBased?: string[];
|
|
79
|
+
freeEmail?: string[];
|
|
80
|
+
abuse?: string[];
|
|
81
|
+
spamtrap?: string[];
|
|
82
|
+
suspect?: string[];
|
|
83
|
+
statusMap?: Record<string, EmailStatusMapEntry>;
|
|
84
|
+
rules?: EmailStatusRule[];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type EmailStatusBuildInput = {
|
|
88
|
+
config: EmailStatusExtractorConfig;
|
|
89
|
+
values: Record<string, unknown>;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const DEFAULT_STATUS_MAP: Record<string, EmailStatusMapEntry> = {
|
|
93
|
+
verified: { status: 'valid', verdict: 'send', verified: true },
|
|
94
|
+
valid: { status: 'valid', verdict: 'send', verified: true },
|
|
95
|
+
deliverable: { status: 'valid', verdict: 'send', verified: true },
|
|
96
|
+
true: { status: 'valid', verdict: 'send', verified: true },
|
|
97
|
+
invalid: { status: 'invalid', verdict: 'drop', verified: false },
|
|
98
|
+
undeliverable: { status: 'invalid', verdict: 'drop', verified: false },
|
|
99
|
+
false: { status: 'invalid', verdict: 'drop', verified: false },
|
|
100
|
+
'catch-all': {
|
|
101
|
+
status: 'catch_all',
|
|
102
|
+
verdict: 'verify_next',
|
|
103
|
+
verified: false,
|
|
104
|
+
},
|
|
105
|
+
catch_all: {
|
|
106
|
+
status: 'catch_all',
|
|
107
|
+
verdict: 'verify_next',
|
|
108
|
+
verified: false,
|
|
109
|
+
},
|
|
110
|
+
valid_catch_all: {
|
|
111
|
+
status: 'valid_catch_all',
|
|
112
|
+
verdict: 'send_with_caution',
|
|
113
|
+
verified: true,
|
|
114
|
+
},
|
|
115
|
+
accept_all: {
|
|
116
|
+
status: 'catch_all',
|
|
117
|
+
verdict: 'verify_next',
|
|
118
|
+
verified: false,
|
|
119
|
+
},
|
|
120
|
+
unknown: { status: 'unknown', verdict: 'hold', verified: false },
|
|
121
|
+
unavailable: { status: 'unknown', verdict: 'hold', verified: false },
|
|
122
|
+
do_not_mail: { status: 'do_not_mail', verdict: 'drop', verified: false },
|
|
123
|
+
spamtrap: { status: 'spamtrap', verdict: 'drop', verified: false },
|
|
124
|
+
abuse: { status: 'abuse', verdict: 'drop', verified: false },
|
|
125
|
+
disposable: { status: 'disposable', verdict: 'drop', verified: false },
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
function normalizeKey(value: unknown): string | null {
|
|
129
|
+
if (value == null) return null;
|
|
130
|
+
if (typeof value === 'boolean') return String(value);
|
|
131
|
+
const normalized = String(value).trim().toLowerCase().replace(/\s+/g, '_');
|
|
132
|
+
return normalized || null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function boolish(value: unknown): boolean | null {
|
|
136
|
+
if (typeof value === 'boolean') return value;
|
|
137
|
+
if (typeof value === 'number') return value === 1 ? true : value === 0 ? false : null;
|
|
138
|
+
if (typeof value !== 'string') return null;
|
|
139
|
+
const normalized = value.trim().toLowerCase();
|
|
140
|
+
if (['true', 'yes', 'y', '1'].includes(normalized)) return true;
|
|
141
|
+
if (['false', 'no', 'n', '0'].includes(normalized)) return false;
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function numberish(value: unknown): number | null {
|
|
146
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
147
|
+
if (typeof value !== 'string' || value.trim() === '') return null;
|
|
148
|
+
const parsed = Number(value);
|
|
149
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function stringish(value: unknown): string | null {
|
|
153
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function deliverability(value: unknown): EmailDeliverability {
|
|
157
|
+
const normalized = normalizeKey(value);
|
|
158
|
+
return normalized === 'high' || normalized === 'medium' || normalized === 'low'
|
|
159
|
+
? normalized
|
|
160
|
+
: 'unknown';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function mxClass(mxProvider: unknown, mxRecord: unknown): EmailMxClass {
|
|
164
|
+
const haystack = `${stringish(mxProvider) ?? ''} ${stringish(mxRecord) ?? ''}`.toLowerCase();
|
|
165
|
+
if (!haystack.trim()) return 'unknown';
|
|
166
|
+
if (
|
|
167
|
+
/proofpoint|pphosted|mimecast|barracuda|ess\.barracudanetworks|ironport|cisco|iphmx|messagelabs|symantec/.test(
|
|
168
|
+
haystack,
|
|
169
|
+
)
|
|
170
|
+
) {
|
|
171
|
+
return 'security_gateway';
|
|
172
|
+
}
|
|
173
|
+
if (/aspmx\.l\.google|google|g-suite|google workspace/.test(haystack)) {
|
|
174
|
+
return 'workspace_mailbox';
|
|
175
|
+
}
|
|
176
|
+
if (/protection\.outlook|office365|microsoft|outlook|exchange online/.test(haystack)) {
|
|
177
|
+
return 'workspace_mailbox';
|
|
178
|
+
}
|
|
179
|
+
if (/gmail|yahoo|icloud|aol|hotmail/.test(haystack)) return 'consumer_mailbox';
|
|
180
|
+
if (/postfix|exim|sendmail|zimbra|plesk|cpanel|mail\./.test(haystack)) return 'on_prem';
|
|
181
|
+
return 'unknown';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function entryForStatus(
|
|
185
|
+
key: string | null,
|
|
186
|
+
map: Record<string, EmailStatusMapEntry> | undefined,
|
|
187
|
+
): EmailStatusMapEntry | null {
|
|
188
|
+
if (!key) return null;
|
|
189
|
+
return map?.[key] ?? DEFAULT_STATUS_MAP[key] ?? null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function read(values: Record<string, unknown>, name: string): unknown {
|
|
193
|
+
return values[name];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function matchesRule(
|
|
197
|
+
rule: EmailStatusRule,
|
|
198
|
+
values: Record<string, unknown>,
|
|
199
|
+
): boolean {
|
|
200
|
+
return Object.entries(rule.when).every(([key, expected]) => {
|
|
201
|
+
const actual = read(values, key);
|
|
202
|
+
if (key.endsWith('Lt')) {
|
|
203
|
+
const source = numberish(read(values, key.slice(0, -2)));
|
|
204
|
+
return typeof expected === 'number' && source != null && source < expected;
|
|
205
|
+
}
|
|
206
|
+
if (typeof expected === 'boolean') return boolish(actual) === expected;
|
|
207
|
+
if (typeof expected === 'number') return numberish(actual) === expected;
|
|
208
|
+
return normalizeKey(actual) === normalizeKey(expected);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function buildEmailStatus({
|
|
213
|
+
config,
|
|
214
|
+
values,
|
|
215
|
+
}: EmailStatusBuildInput): EmailStatus {
|
|
216
|
+
const rawStatus = read(values, 'rawStatus');
|
|
217
|
+
const rawScore = numberish(read(values, 'rawScore'));
|
|
218
|
+
const valid = boolish(read(values, 'valid'));
|
|
219
|
+
const catchAll = boolish(read(values, 'catchAll'));
|
|
220
|
+
const disposable = boolish(read(values, 'disposable'));
|
|
221
|
+
const abuse = boolish(read(values, 'abuse'));
|
|
222
|
+
const spamtrap = boolish(read(values, 'spamtrap'));
|
|
223
|
+
const suspect = boolish(read(values, 'suspect'));
|
|
224
|
+
const rawKey = normalizeKey(rawStatus);
|
|
225
|
+
const mapped =
|
|
226
|
+
config.rules?.find((rule) => matchesRule(rule, values)) ??
|
|
227
|
+
entryForStatus(rawKey, config.statusMap) ??
|
|
228
|
+
entryForStatus(valid == null ? null : String(valid), config.statusMap);
|
|
229
|
+
|
|
230
|
+
const status =
|
|
231
|
+
mapped?.status ??
|
|
232
|
+
(disposable
|
|
233
|
+
? 'disposable'
|
|
234
|
+
: abuse
|
|
235
|
+
? 'abuse'
|
|
236
|
+
: spamtrap
|
|
237
|
+
? 'spamtrap'
|
|
238
|
+
: catchAll
|
|
239
|
+
? 'catch_all'
|
|
240
|
+
: valid === true
|
|
241
|
+
? 'valid'
|
|
242
|
+
: valid === false
|
|
243
|
+
? 'invalid'
|
|
244
|
+
: 'unknown');
|
|
245
|
+
const defaultVerdict: EmailStatusVerdict =
|
|
246
|
+
status === 'valid'
|
|
247
|
+
? 'send'
|
|
248
|
+
: status === 'valid_catch_all'
|
|
249
|
+
? 'send_with_caution'
|
|
250
|
+
: status === 'catch_all'
|
|
251
|
+
? 'verify_next'
|
|
252
|
+
: status === 'unknown'
|
|
253
|
+
? 'hold'
|
|
254
|
+
: 'drop';
|
|
255
|
+
const verdict = mapped?.verdict ?? defaultVerdict;
|
|
256
|
+
const verified =
|
|
257
|
+
mapped?.verified ??
|
|
258
|
+
(status === 'valid' || status === 'valid_catch_all' || verdict === 'send');
|
|
259
|
+
const reasons = [
|
|
260
|
+
mapped?.reason,
|
|
261
|
+
catchAll ? 'catch_all_domain' : null,
|
|
262
|
+
mxClass(read(values, 'mxProvider'), read(values, 'mxRecord')) ===
|
|
263
|
+
'security_gateway'
|
|
264
|
+
? 'security_gateway_mx'
|
|
265
|
+
: null,
|
|
266
|
+
suspect ? 'provider_marked_suspect' : null,
|
|
267
|
+
].filter((reason): reason is string => typeof reason === 'string');
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
verdict,
|
|
271
|
+
status,
|
|
272
|
+
verified,
|
|
273
|
+
confidence: rawScore,
|
|
274
|
+
reasons,
|
|
275
|
+
signals: {
|
|
276
|
+
catch_all: catchAll,
|
|
277
|
+
deliverability: deliverability(read(values, 'deliverability')),
|
|
278
|
+
mx_class: mxClass(read(values, 'mxProvider'), read(values, 'mxRecord')),
|
|
279
|
+
mx_provider: stringish(read(values, 'mxProvider')),
|
|
280
|
+
mx_record: stringish(read(values, 'mxRecord')),
|
|
281
|
+
fraud_score: numberish(read(values, 'fraudScore')),
|
|
282
|
+
disposable,
|
|
283
|
+
role_based: boolish(read(values, 'roleBased')),
|
|
284
|
+
free_email: boolish(read(values, 'freeEmail')),
|
|
285
|
+
abuse,
|
|
286
|
+
spamtrap,
|
|
287
|
+
suspect,
|
|
288
|
+
valid,
|
|
289
|
+
},
|
|
290
|
+
provider: {
|
|
291
|
+
name: config.provider,
|
|
292
|
+
raw_status:
|
|
293
|
+
typeof rawStatus === 'string' ||
|
|
294
|
+
typeof rawStatus === 'boolean' ||
|
|
295
|
+
typeof rawStatus === 'number'
|
|
296
|
+
? rawStatus
|
|
297
|
+
: null,
|
|
298
|
+
raw_score: rawScore,
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
@@ -3,11 +3,13 @@ export type StepProgramDatasetOptions = {
|
|
|
3
3
|
row: Record<string, unknown>,
|
|
4
4
|
index: number,
|
|
5
5
|
) => boolean | Promise<boolean>;
|
|
6
|
+
staleAfterSeconds?: number;
|
|
6
7
|
};
|
|
7
8
|
|
|
8
9
|
export type StepProgramDatasetStep<TResolver> = {
|
|
9
10
|
name: string;
|
|
10
11
|
resolver: TResolver;
|
|
12
|
+
staleAfterSeconds?: number;
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
export type StepProgramDatasetProgram<TStep> = {
|
|
@@ -84,6 +86,9 @@ export class StepProgramDatasetBuilder<
|
|
|
84
86
|
{
|
|
85
87
|
name,
|
|
86
88
|
resolver: this.applyDerivationOptions(resolver, options),
|
|
89
|
+
...(options?.staleAfterSeconds !== undefined
|
|
90
|
+
? { staleAfterSeconds: options.staleAfterSeconds }
|
|
91
|
+
: {}),
|
|
87
92
|
} as TStep,
|
|
88
93
|
];
|
|
89
94
|
return this;
|
|
@@ -9,7 +9,15 @@ export type {
|
|
|
9
9
|
ToolResultTargetMetadata,
|
|
10
10
|
ToolResultTargetAccessor,
|
|
11
11
|
} from './tool-result-types';
|
|
12
|
-
|
|
12
|
+
export type {
|
|
13
|
+
EmailDeliverability,
|
|
14
|
+
EmailMxClass,
|
|
15
|
+
EmailStatus,
|
|
16
|
+
EmailStatusValue,
|
|
17
|
+
EmailStatusVerdict,
|
|
18
|
+
} from './email-status';
|
|
19
|
+
|
|
20
|
+
import { buildEmailStatus } from './email-status';
|
|
13
21
|
import type {
|
|
14
22
|
SerializedToolExecuteResult,
|
|
15
23
|
ToolExecuteResult,
|
|
@@ -302,6 +310,50 @@ function findFirstTargetByPath(
|
|
|
302
310
|
return null;
|
|
303
311
|
}
|
|
304
312
|
|
|
313
|
+
function firstValueForPaths(
|
|
314
|
+
result: unknown,
|
|
315
|
+
paths: readonly string[] | undefined,
|
|
316
|
+
): ToolResultTargetMetadata | null {
|
|
317
|
+
return findFirstTargetByPath(result, paths);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function buildEmailStatusTarget(
|
|
321
|
+
result: unknown,
|
|
322
|
+
descriptor: ToolResultExtractorDescriptor,
|
|
323
|
+
): ToolResultTargetMetadata | null {
|
|
324
|
+
const config = descriptor.emailStatus;
|
|
325
|
+
if (!config) return null;
|
|
326
|
+
const values: Record<string, unknown> = {};
|
|
327
|
+
const pathSets: Record<string, readonly string[] | undefined> = {
|
|
328
|
+
rawStatus: config.rawStatus,
|
|
329
|
+
rawScore: config.rawScore,
|
|
330
|
+
valid: config.valid,
|
|
331
|
+
deliverability: config.deliverability,
|
|
332
|
+
catchAll: config.catchAll,
|
|
333
|
+
mxProvider: config.mxProvider,
|
|
334
|
+
mxRecord: config.mxRecord,
|
|
335
|
+
fraudScore: config.fraudScore,
|
|
336
|
+
disposable: config.disposable,
|
|
337
|
+
roleBased: config.roleBased,
|
|
338
|
+
freeEmail: config.freeEmail,
|
|
339
|
+
abuse: config.abuse,
|
|
340
|
+
spamtrap: config.spamtrap,
|
|
341
|
+
suspect: config.suspect,
|
|
342
|
+
};
|
|
343
|
+
let firstPath: string | null = null;
|
|
344
|
+
for (const [name, paths] of Object.entries(pathSets)) {
|
|
345
|
+
const match = firstValueForPaths(result, paths);
|
|
346
|
+
if (!match) continue;
|
|
347
|
+
values[name] = match.value;
|
|
348
|
+
firstPath ??= match.path;
|
|
349
|
+
}
|
|
350
|
+
if (!firstPath) return null;
|
|
351
|
+
return {
|
|
352
|
+
path: firstPath,
|
|
353
|
+
value: buildEmailStatus({ config, values }),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
305
357
|
function findFirstTargetByKey(
|
|
306
358
|
result: unknown,
|
|
307
359
|
target: string,
|
|
@@ -507,6 +559,11 @@ function buildTargets(
|
|
|
507
559
|
): Record<string, ToolResultTargetMetadata> {
|
|
508
560
|
const targets: Record<string, ToolResultTargetMetadata> = {};
|
|
509
561
|
for (const [target, descriptor] of Object.entries(extractors ?? {})) {
|
|
562
|
+
const emailStatusTarget = buildEmailStatusTarget(result, descriptor);
|
|
563
|
+
if (emailStatusTarget) {
|
|
564
|
+
targets[target] = emailStatusTarget;
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
510
567
|
const fromExtractor = findFirstTargetByPath(result, descriptor.paths);
|
|
511
568
|
if (!fromExtractor) continue;
|
|
512
569
|
const transformed = coerceToEnum(
|
|
@@ -192,46 +192,6 @@ export function normalizeTableNamespace(value: string): string {
|
|
|
192
192
|
);
|
|
193
193
|
}
|
|
194
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.dataset() 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
195
|
export function validatePlaySheetTableName(
|
|
236
196
|
playName: string,
|
|
237
197
|
tableNamespace: string,
|