@oxygen-agent/cli 1.152.15 → 1.160.18

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,501 @@
1
+ import { OxygenError } from "./index.js";
2
+ import { renderLinkedInTemplate } from "./linkedin-sequences.js";
3
+ /**
4
+ * Multichannel sequence DSL — the shared contract validated identically by CLI,
5
+ * MCP, API, and web. A sequence is an ordered list of steps applied to each
6
+ * enrolled lead, spanning LinkedIn (executed natively by the sequencer dispatch
7
+ * engine) and email (native send through a mailbox, or delegated to a bound
8
+ * Instantly campaign). Three control kinds make it cross-channel:
9
+ *
10
+ * - wait_for_signal — gate until a provider signal fires (LinkedIn connection
11
+ * accepted, email opened/replied, ...) or timeout_days elapse; generalizes
12
+ * the LinkedIn-only wait_for_connection gate.
13
+ * - branch — evaluate a condition over the signals an enrollment has
14
+ * accumulated and jump to a target step (then) or fall through (else).
15
+ * - stop — explicit terminal step, the only way to give a branch's two arms
16
+ * genuinely distinct endings.
17
+ *
18
+ * A LinkedIn-only sequence (channels = ['linkedin'], no email steps) is exactly
19
+ * the behavior of the original LinkedIn sequencer, so the same engine and tables
20
+ * back both. This module is the single home for the schema; the legacy
21
+ * `linkedin-sequences.ts` validator remains for back-compat reads of stored
22
+ * LinkedIn-only definitions.
23
+ *
24
+ * Email steps: email_send/email_reply send natively through one of the
25
+ * sequence's mailboxes (Gmail API / Microsoft Graph) — Oxygen owns the send.
26
+ * email_enroll/move/stop delegate to a bound Instantly campaign (legacy, removed
27
+ * once native send fully replaces them); Instantly owns send cadence, warmup,
28
+ * and inbox rotation while Oxygen owns the cross-channel timeline + reply
29
+ * suppression.
30
+ */
31
+ export const SEQUENCE_CHANNELS = ["linkedin", "email"];
32
+ /**
33
+ * Signals an enrollment accumulates from provider webhooks. wait_for_signal gates
34
+ * and signal-branch conditions read these. linkedin_connected also advances the
35
+ * legacy wait_for_connection gate. (The lead's current connection *degree* is NOT
36
+ * a signal — it's resolved on demand by the dispatcher via a connection branch,
37
+ * `condition: "already_connected"`.)
38
+ */
39
+ export const SEQUENCE_SIGNALS = [
40
+ "linkedin_connected",
41
+ "linkedin_replied",
42
+ "email_sent",
43
+ "email_opened",
44
+ "email_clicked",
45
+ "email_replied",
46
+ "email_bounced",
47
+ ];
48
+ /**
49
+ * row_values keys an enrollment's email may live under, in send-precedence order.
50
+ * The dispatcher picks the FIRST present key as the recipient address; the
51
+ * enrollment-lookup queries OR across all of them (order-independent there).
52
+ * Single source of truth so the send path and the lookup path can't drift.
53
+ */
54
+ export const SEQUENCE_EMAIL_COLUMN_KEYS = ["email", "email_address", "work_email", "primary_email", "Email"];
55
+ export const SEQUENCE_STEP_KINDS = [
56
+ // linkedin channel (native dispatch)
57
+ "visit_profile",
58
+ "invite",
59
+ "wait_for_connection",
60
+ "message",
61
+ "inmail",
62
+ // email channel
63
+ "email_send",
64
+ "email_reply",
65
+ "email_enroll",
66
+ "email_move",
67
+ "email_stop",
68
+ // control (channel-agnostic)
69
+ "wait",
70
+ "wait_for_signal",
71
+ "branch",
72
+ "stop",
73
+ ];
74
+ /**
75
+ * Conditions a connection branch routes on. Both need live LinkedIn access, so
76
+ * the dispatcher (not the planner) resolves them over time:
77
+ * - connection_accepted: wait until the invite is accepted (or timeout_days) then
78
+ * route accepted → then_id, timeout → else_id.
79
+ * - already_connected: resolve the lead's current 1st-degree state at entry and
80
+ * route connected → then_id, not → else_id (no waiting).
81
+ */
82
+ export const SEQUENCE_CONNECTION_BRANCH_CONDITIONS = ["connection_accepted", "already_connected"];
83
+ const MAX_SEQUENCE_STEPS = 50;
84
+ const MAX_TEMPLATE_LENGTH = 8_000;
85
+ const MAX_NOTE_LENGTH = 300;
86
+ const ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
87
+ /** Which channel a step executes on, or null for channel-agnostic control steps. */
88
+ export function sequenceStepChannel(step) {
89
+ return channelForKind(step.kind);
90
+ }
91
+ /** Distinct channels a sequence touches (drives the sequence's channels[] tag). */
92
+ export function sequenceChannels(definition) {
93
+ const channels = new Set();
94
+ for (const step of definition.steps) {
95
+ const channel = sequenceStepChannel(step);
96
+ if (channel)
97
+ channels.add(channel);
98
+ }
99
+ return [...channels];
100
+ }
101
+ /**
102
+ * Validate + normalize a raw sequence definition. Assigns stable ids to steps
103
+ * that omit one (s{index}), verifies branch/gate targets resolve, and enforces
104
+ * per-step shapes + channel restrictions. Returns the normalized definition or
105
+ * throws OxygenError("invalid_sequence") with per-step issues. Pure.
106
+ */
107
+ export function validateSequenceDefinition(input, options = {}) {
108
+ const issues = [];
109
+ const rawSteps = collectSteps(input, issues);
110
+ // Pass 1: normalize each step + assign ids.
111
+ const normalized = [];
112
+ const usedIds = new Set();
113
+ rawSteps.forEach((rawStep, index) => {
114
+ const step = normalizeStep(rawStep, index, options, issues);
115
+ if (!step)
116
+ return;
117
+ if (usedIds.has(step.id)) {
118
+ issues.push({ path: `steps[${index}].id`, message: `Duplicate step id '${step.id}'.` });
119
+ }
120
+ usedIds.add(step.id);
121
+ normalized.push(step);
122
+ });
123
+ // Pass 2: branch targets must resolve to an existing step id (connection
124
+ // branches route by then_id/else_id, signal branches by then/else).
125
+ normalized.forEach((step, index) => {
126
+ if (step.kind !== "branch")
127
+ return;
128
+ if (typeof step.condition === "string") {
129
+ for (const [key, target] of [["then_id", step.then_id], ["else_id", step.else_id]]) {
130
+ if (target && !usedIds.has(target)) {
131
+ issues.push({ path: `steps[${index}].${key}`, message: `branch target '${target}' is not a step id.` });
132
+ }
133
+ }
134
+ }
135
+ else {
136
+ for (const [key, target] of [["then", step.then], ["else", step.else]]) {
137
+ if (target !== null && !usedIds.has(target)) {
138
+ issues.push({ path: `steps[${index}].${key}`, message: `branch target '${target}' is not a step id.` });
139
+ }
140
+ }
141
+ }
142
+ });
143
+ if (issues.length > 0) {
144
+ throw new OxygenError("invalid_sequence", `Sequence definition is invalid: ${issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`, { details: { issues }, exitCode: 1 });
145
+ }
146
+ return { steps: normalized };
147
+ }
148
+ /** Non-throwing variant for lint surfaces. */
149
+ export function lintSequenceDefinition(input, options = {}) {
150
+ try {
151
+ validateSequenceDefinition(input, options);
152
+ return [];
153
+ }
154
+ catch (error) {
155
+ if (error instanceof OxygenError && error.details && typeof error.details === "object") {
156
+ const issues = error.details.issues;
157
+ if (Array.isArray(issues))
158
+ return issues;
159
+ }
160
+ return [{ path: "steps", message: error instanceof Error ? error.message : "Invalid sequence." }];
161
+ }
162
+ }
163
+ function collectSteps(input, issues) {
164
+ const record = isRecord(input) ? input : null;
165
+ const steps = record?.steps;
166
+ if (!Array.isArray(steps)) {
167
+ issues.push({ path: "steps", message: "steps must be an array." });
168
+ return [];
169
+ }
170
+ if (steps.length === 0) {
171
+ issues.push({ path: "steps", message: "A sequence needs at least one step." });
172
+ }
173
+ if (steps.length > MAX_SEQUENCE_STEPS) {
174
+ issues.push({ path: "steps", message: `A sequence may have at most ${MAX_SEQUENCE_STEPS} steps.` });
175
+ }
176
+ return steps;
177
+ }
178
+ function normalizeStep(// skipcq: JS-R1005 -- discriminated DSL: one normalizer per step kind.
179
+ raw, index, options, issues) {
180
+ const path = `steps[${index}]`;
181
+ if (!isRecord(raw)) {
182
+ issues.push({ path, message: "Each step must be an object." });
183
+ return null;
184
+ }
185
+ const kind = raw.kind;
186
+ if (typeof kind !== "string" || !SEQUENCE_STEP_KINDS.includes(kind)) {
187
+ issues.push({ path: `${path}.kind`, message: `kind must be one of: ${SEQUENCE_STEP_KINDS.join(", ")}.` });
188
+ return null;
189
+ }
190
+ const id = normalizeId(raw.id, index, path, issues);
191
+ const stepKind = kind;
192
+ const channel = channelForKind(stepKind);
193
+ if (channel && options.allowedChannels && !options.allowedChannels.includes(channel)) {
194
+ issues.push({ path: `${path}.channel`, message: `channel '${channel}' is not enabled for this sequence.` });
195
+ }
196
+ switch (stepKind) {
197
+ case "visit_profile":
198
+ return { id, channel: "linkedin", kind: "visit_profile" };
199
+ case "invite": {
200
+ const note = optionalTemplate(raw.note_template, `${path}.note_template`, MAX_NOTE_LENGTH, issues);
201
+ return note !== undefined
202
+ ? { id, channel: "linkedin", kind: "invite", note_template: note }
203
+ : { id, channel: "linkedin", kind: "invite" };
204
+ }
205
+ case "wait_for_connection": {
206
+ const timeoutDays = raw.timeout_days === undefined || raw.timeout_days === null
207
+ ? 14
208
+ : positiveInt(raw.timeout_days, `${path}.timeout_days`, issues) ?? 14;
209
+ return {
210
+ id,
211
+ channel: "linkedin",
212
+ kind: "wait_for_connection",
213
+ timeout_days: timeoutDays,
214
+ on_timeout: raw.on_timeout === "continue" ? "continue" : "stop",
215
+ };
216
+ }
217
+ case "message": {
218
+ const template = requiredTemplate(raw.template, `${path}.template`, issues);
219
+ return { id, channel: "linkedin", kind: "message", template: template ?? "" };
220
+ }
221
+ case "inmail": {
222
+ const subject = requiredTemplate(raw.subject_template, `${path}.subject_template`, issues);
223
+ const template = requiredTemplate(raw.template, `${path}.template`, issues);
224
+ return { id, channel: "linkedin", kind: "inmail", subject_template: subject ?? "", template: template ?? "" };
225
+ }
226
+ case "email_enroll": {
227
+ const subsequenceId = optionalString(raw.subsequence_id, `${path}.subsequence_id`, issues);
228
+ return subsequenceId !== undefined
229
+ ? { id, channel: "email", kind: "email_enroll", subsequence_id: subsequenceId }
230
+ : { id, channel: "email", kind: "email_enroll" };
231
+ }
232
+ case "email_move": {
233
+ const subsequenceId = optionalString(raw.subsequence_id, `${path}.subsequence_id`, issues);
234
+ if (subsequenceId === undefined) {
235
+ issues.push({ path: `${path}.subsequence_id`, message: "email_move requires subsequence_id." });
236
+ }
237
+ return { id, channel: "email", kind: "email_move", subsequence_id: subsequenceId ?? "" };
238
+ }
239
+ case "email_send": {
240
+ const subject = requiredTemplate(raw.subject_template, `${path}.subject_template`, issues);
241
+ const body = requiredTemplate(raw.body_template, `${path}.body_template`, issues);
242
+ return { id, channel: "email", kind: "email_send", subject_template: subject ?? "", body_template: body ?? "" };
243
+ }
244
+ case "email_reply": {
245
+ const body = requiredTemplate(raw.body_template, `${path}.body_template`, issues);
246
+ return { id, channel: "email", kind: "email_reply", body_template: body ?? "" };
247
+ }
248
+ case "email_stop":
249
+ return { id, channel: "email", kind: "email_stop" };
250
+ case "wait": {
251
+ const days = optionalNonNegativeInt(raw.days, `${path}.days`, issues);
252
+ const hours = optionalNonNegativeInt(raw.hours, `${path}.hours`, issues);
253
+ if ((days ?? 0) + (hours ?? 0) <= 0) {
254
+ issues.push({ path, message: "A wait step needs days and/or hours totaling at least 1 hour." });
255
+ }
256
+ return {
257
+ id,
258
+ kind: "wait",
259
+ ...(days !== undefined ? { days } : {}),
260
+ ...(hours !== undefined ? { hours } : {}),
261
+ };
262
+ }
263
+ case "wait_for_signal": {
264
+ const signal = normalizeSignal(raw.signal, `${path}.signal`, issues);
265
+ const timeoutDays = raw.timeout_days === undefined || raw.timeout_days === null
266
+ ? 7
267
+ : positiveInt(raw.timeout_days, `${path}.timeout_days`, issues) ?? 7;
268
+ return {
269
+ id,
270
+ kind: "wait_for_signal",
271
+ signal: signal ?? "email_replied",
272
+ timeout_days: timeoutDays,
273
+ on_timeout: raw.on_timeout === "continue" ? "continue" : "stop",
274
+ };
275
+ }
276
+ case "branch": {
277
+ // String condition → LinkedIn connection branch (preserve verbatim; the
278
+ // dispatcher resolves accept-wait / degree over time, routing then_id/else_id).
279
+ if (typeof raw.condition === "string") {
280
+ if (!SEQUENCE_CONNECTION_BRANCH_CONDITIONS.includes(raw.condition)) {
281
+ issues.push({
282
+ path: `${path}.condition`,
283
+ message: `string condition must be one of: ${SEQUENCE_CONNECTION_BRANCH_CONDITIONS.join(", ")} (or a signal-condition object).`,
284
+ });
285
+ return null;
286
+ }
287
+ const conditionStr = raw.condition;
288
+ const timeoutDays = raw.timeout_days === undefined || raw.timeout_days === null
289
+ ? undefined
290
+ : positiveInt(raw.timeout_days, `${path}.timeout_days`, issues);
291
+ const thenId = normalizeTargetRef(raw.then_id ?? raw.then, `${path}.then_id`, issues);
292
+ const elseId = normalizeTargetRef(raw.else_id ?? raw.else, `${path}.else_id`, issues);
293
+ return {
294
+ id,
295
+ kind: "branch",
296
+ condition: conditionStr,
297
+ ...(timeoutDays !== undefined
298
+ ? { timeout_days: timeoutDays }
299
+ : conditionStr === "connection_accepted" ? { timeout_days: 14 } : {}),
300
+ ...(thenId !== null ? { then_id: thenId } : {}),
301
+ ...(elseId !== null ? { else_id: elseId } : {}),
302
+ };
303
+ }
304
+ // Object condition → signal branch. Accept then_id/else_id as aliases.
305
+ const condition = normalizeCondition(raw.condition, `${path}.condition`, issues);
306
+ return {
307
+ id,
308
+ kind: "branch",
309
+ condition: condition ?? { signal: "email_replied", present: true },
310
+ then: normalizeTargetRef(raw.then ?? raw.then_id, `${path}.then`, issues),
311
+ else: normalizeTargetRef(raw.else ?? raw.else_id, `${path}.else`, issues),
312
+ };
313
+ }
314
+ case "stop":
315
+ return { id, kind: "stop" };
316
+ default:
317
+ return null;
318
+ }
319
+ }
320
+ function channelForKind(kind) {
321
+ if (kind === "wait" || kind === "wait_for_signal" || kind === "branch" || kind === "stop")
322
+ return null;
323
+ if (kind === "email_send" || kind === "email_reply" ||
324
+ kind === "email_enroll" || kind === "email_move" || kind === "email_stop")
325
+ return "email";
326
+ return "linkedin";
327
+ }
328
+ function normalizeId(value, index, path, issues) {
329
+ if (value === undefined || value === null)
330
+ return `s${index}`;
331
+ if (typeof value !== "string" || !ID_PATTERN.test(value)) {
332
+ issues.push({ path: `${path}.id`, message: "id must match [A-Za-z0-9_-]{1,64}." });
333
+ return `s${index}`;
334
+ }
335
+ return value;
336
+ }
337
+ function normalizeSignal(value, path, issues) {
338
+ if (typeof value !== "string" || !SEQUENCE_SIGNALS.includes(value)) {
339
+ issues.push({ path, message: `signal must be one of: ${SEQUENCE_SIGNALS.join(", ")}.` });
340
+ return undefined;
341
+ }
342
+ return value;
343
+ }
344
+ function normalizeTargetRef(value, path, issues) {
345
+ if (value === undefined || value === null)
346
+ return null;
347
+ if (typeof value !== "string" || !ID_PATTERN.test(value)) {
348
+ issues.push({ path, message: "must be a step id or null." });
349
+ return null;
350
+ }
351
+ return value;
352
+ }
353
+ function normalizeCondition(raw, path, issues, depth = 0) {
354
+ if (depth > 6) {
355
+ issues.push({ path, message: "condition nests too deeply." });
356
+ return undefined;
357
+ }
358
+ if (!isRecord(raw)) {
359
+ issues.push({ path, message: "condition must be an object." });
360
+ return undefined;
361
+ }
362
+ if (typeof raw.signal === "string") {
363
+ const signal = normalizeSignal(raw.signal, `${path}.signal`, issues);
364
+ if (!signal)
365
+ return undefined;
366
+ return { signal, present: raw.present === false ? false : true };
367
+ }
368
+ if (Array.isArray(raw.all)) {
369
+ return { all: normalizeConditionList(raw.all, `${path}.all`, issues, depth) };
370
+ }
371
+ if (Array.isArray(raw.any)) {
372
+ return { any: normalizeConditionList(raw.any, `${path}.any`, issues, depth) };
373
+ }
374
+ if (raw.not !== undefined) {
375
+ const inner = normalizeCondition(raw.not, `${path}.not`, issues, depth + 1);
376
+ return inner ? { not: inner } : undefined;
377
+ }
378
+ issues.push({ path, message: "condition must be {signal}, {all}, {any}, or {not}." });
379
+ return undefined;
380
+ }
381
+ function normalizeConditionList(raw, path, issues, depth) {
382
+ if (raw.length === 0) {
383
+ issues.push({ path, message: "must have at least one condition." });
384
+ }
385
+ const out = [];
386
+ raw.forEach((entry, index) => {
387
+ const condition = normalizeCondition(entry, `${path}[${index}]`, issues, depth + 1);
388
+ if (condition)
389
+ out.push(condition);
390
+ });
391
+ return out;
392
+ }
393
+ function requiredTemplate(value, path, issues) {
394
+ if (typeof value !== "string" || !value.trim()) {
395
+ issues.push({ path, message: "is required and must be a non-empty string." });
396
+ return undefined;
397
+ }
398
+ if (value.length > MAX_TEMPLATE_LENGTH) {
399
+ issues.push({ path, message: `must be at most ${MAX_TEMPLATE_LENGTH} characters.` });
400
+ return undefined;
401
+ }
402
+ return value;
403
+ }
404
+ function optionalTemplate(value, path, maxLength, issues) {
405
+ if (value === undefined || value === null)
406
+ return undefined;
407
+ if (typeof value !== "string") {
408
+ issues.push({ path, message: "must be a string." });
409
+ return undefined;
410
+ }
411
+ if (value.length > maxLength) {
412
+ issues.push({ path, message: `must be at most ${maxLength} characters.` });
413
+ return undefined;
414
+ }
415
+ return value;
416
+ }
417
+ function optionalString(value, path, issues) {
418
+ if (value === undefined || value === null)
419
+ return undefined;
420
+ if (typeof value !== "string" || !value.trim()) {
421
+ issues.push({ path, message: "must be a non-empty string." });
422
+ return undefined;
423
+ }
424
+ return value;
425
+ }
426
+ function positiveInt(value, path, issues) {
427
+ const num = Number(value);
428
+ if (!Number.isInteger(num) || num <= 0) {
429
+ issues.push({ path, message: "must be a positive integer." });
430
+ return undefined;
431
+ }
432
+ return num;
433
+ }
434
+ function optionalNonNegativeInt(value, path, issues) {
435
+ if (value === undefined || value === null)
436
+ return undefined;
437
+ const num = Number(value);
438
+ if (!Number.isInteger(num) || num < 0) {
439
+ issues.push({ path, message: "must be a non-negative integer." });
440
+ return undefined;
441
+ }
442
+ return num;
443
+ }
444
+ /** Total delay in milliseconds a wait step introduces. */
445
+ export function sequenceWaitStepDelayMs(step) {
446
+ const days = step.days ?? 0;
447
+ const hours = step.hours ?? 0;
448
+ return (days * 24 + hours) * 60 * 60 * 1000;
449
+ }
450
+ /** Render a {{column}} template against a row's values (reuses the LinkedIn impl). */
451
+ export function renderSequenceTemplate(template, values) {
452
+ return renderLinkedInTemplate(template, values);
453
+ }
454
+ /** Column keys referenced by {{...}} placeholders across every step's copy. */
455
+ export function sequenceTemplateVariables(definition) {
456
+ const vars = new Set();
457
+ const scan = (template) => {
458
+ if (!template)
459
+ return;
460
+ for (const match of template.matchAll(/\{\{\s*([\w.]+)\s*\}\}/g)) {
461
+ if (match[1])
462
+ vars.add(match[1]);
463
+ }
464
+ };
465
+ for (const step of definition.steps) {
466
+ if (step.kind === "invite")
467
+ scan(step.note_template);
468
+ if (step.kind === "message")
469
+ scan(step.template);
470
+ if (step.kind === "inmail") {
471
+ scan(step.subject_template);
472
+ scan(step.template);
473
+ }
474
+ if (step.kind === "email_send") {
475
+ scan(step.subject_template);
476
+ scan(step.body_template);
477
+ }
478
+ if (step.kind === "email_reply")
479
+ scan(step.body_template);
480
+ }
481
+ return [...vars];
482
+ }
483
+ /**
484
+ * Evaluate a condition against the set of signals an enrollment has fired.
485
+ * Used by the dispatch planner for branch steps. Pure.
486
+ */
487
+ export function evaluateSequenceCondition(condition, firedSignals) {
488
+ const fired = firedSignals instanceof Set ? firedSignals : new Set(firedSignals);
489
+ if ("signal" in condition) {
490
+ const present = fired.has(condition.signal);
491
+ return condition.present === false ? !present : present;
492
+ }
493
+ if ("all" in condition)
494
+ return condition.all.every((c) => evaluateSequenceCondition(c, fired));
495
+ if ("any" in condition)
496
+ return condition.any.some((c) => evaluateSequenceCondition(c, fired));
497
+ return !evaluateSequenceCondition(condition.not, fired);
498
+ }
499
+ function isRecord(value) {
500
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
501
+ }
@@ -0,0 +1,43 @@
1
+ /** Normalized failure category, derived from SQLSTATE first, then errno/message. */
2
+ export type SqlErrorCause = "connect_timeout" | "connection" | "statement_timeout" | "admin_shutdown" | "too_many_connections" | "insufficient_resources" | "auth" | "schema_drift" | "data_exception" | "integrity_constraint" | "serialization" | "deadlock" | "transaction_rollback" | "syntax_or_access" | "internal" | "unknown";
3
+ export type SqlErrorAttribution = {
4
+ /** Postgres SQLSTATE (5 chars) when present; null for connection-level errnos. */
5
+ pgCode: string | null;
6
+ cause: SqlErrorCause;
7
+ /** Leading SQL verb (select/insert/update/...) parsed from parameter-free SQL. */
8
+ queryKind: string | null;
9
+ /** Schema-qualified table identifier (schema metadata, never row data). */
10
+ table: string | null;
11
+ /** True when a retry on the next tick could plausibly succeed. */
12
+ transient: boolean;
13
+ /** pg severity keyword (ERROR/FATAL/PANIC/WARNING); null when absent. */
14
+ severity: string | null;
15
+ /** Schema name from the pg error (schema metadata, never row data). */
16
+ schema: string | null;
17
+ /** Column name implicated by the failure (NOT NULL / type violations). */
18
+ column: string | null;
19
+ /** Constraint name implicated by the failure (unique/foreign-key/check). */
20
+ constraint: string | null;
21
+ /** Neon/HTTP transport status code (numeric) when the failure carries one. */
22
+ statusCode: number | null;
23
+ };
24
+ /**
25
+ * Classifies a pg/drizzle error into structured, non-secret attribution.
26
+ * Returns null when the error carries no SQL/connection signal at all (so
27
+ * non-DB errors never gain spurious `db.*` attributes). Never throws.
28
+ */
29
+ export declare function describeSqlError(error: unknown): SqlErrorAttribution | null;
30
+ /** Log-field shape (snake_case) for merging into `errorFields(error)` payloads. */
31
+ export declare function sqlErrorFields(error: unknown): Record<string, unknown>;
32
+ /**
33
+ * Strips drizzle's `\nparams: <values>` segment from an error message or stack
34
+ * so SQL parameter values never reach logs/telemetry (OXY-46). Preserves the SQL
35
+ * text itself and any following stack frames; a no-op on non-drizzle text.
36
+ *
37
+ * drizzle's DrizzleQueryError carries the parameter array inside its `.message`
38
+ * (and therefore its `.stack`) as `Failed query: <sql>\nparams: <values>`, so
39
+ * the generic error serializers would otherwise emit those values verbatim.
40
+ */
41
+ export declare function redactSqlParameters(text: string): string;
42
+ /** Telemetry-attribute shape (dotted keys) for span error attribution. */
43
+ export declare function sqlErrorTelemetryAttributes(error: unknown): Record<string, unknown>;