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.
@@ -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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.1.74",
3
+ "version": "0.1.77",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {