@oxygen-agent/cli 1.1014.1138 → 1.1014.1221

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +2 -0
  3. package/dist/command-support.d.ts +8 -0
  4. package/dist/command-support.js +11 -0
  5. package/dist/commands/api-keys.d.ts +9 -0
  6. package/dist/commands/api-keys.js +4 -1
  7. package/dist/commands/capabilities.js +4 -2
  8. package/dist/commands/crm.js +28 -2
  9. package/dist/commands/dashboards.js +2 -2
  10. package/dist/commands/db.js +34 -0
  11. package/dist/commands/find.js +1 -1
  12. package/dist/commands/knowledge.js +2 -0
  13. package/dist/commands/mailboxes.js +3 -2
  14. package/dist/commands/messages.js +6 -1
  15. package/dist/commands/senders.js +2 -0
  16. package/dist/commands/sequences.js +1 -1
  17. package/dist/commands/signals.js +11 -2
  18. package/dist/deliverability-evidence-commands.d.ts +4 -0
  19. package/dist/deliverability-evidence-commands.js +104 -0
  20. package/dist/diagnostic-context.d.ts +21 -0
  21. package/dist/diagnostic-context.js +25 -0
  22. package/dist/diagnostics.d.ts +1 -3
  23. package/dist/diagnostics.js +12 -30
  24. package/dist/http-client.js +1 -1
  25. package/dist/network-mining-host.d.ts +6 -0
  26. package/dist/network-mining-host.js +10 -0
  27. package/node_modules/@oxygen/formula/dist/value-normalizers.d.ts +1 -1
  28. package/node_modules/@oxygen/formula/dist/value-normalizers.js +5 -0
  29. package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +169 -0
  30. package/node_modules/@oxygen/shared/dist/agent-sessions.js +17 -0
  31. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
  32. package/node_modules/@oxygen/shared/dist/capability-discovery.js +114 -5
  33. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -1
  34. package/node_modules/@oxygen/shared/dist/contracts/operation-request.d.ts +92 -0
  35. package/node_modules/@oxygen/shared/dist/contracts/operation-request.js +36 -0
  36. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +3 -90
  37. package/node_modules/@oxygen/shared/dist/contracts/operations.js +5 -39
  38. package/node_modules/@oxygen/shared/dist/contracts/reply-oxygent.d.ts +2 -0
  39. package/node_modules/@oxygen/shared/dist/contracts/reply-oxygent.js +123 -0
  40. package/node_modules/@oxygen/shared/dist/contracts/teammates.js +1 -0
  41. package/node_modules/@oxygen/shared/dist/crm-activity-events.d.ts +4 -0
  42. package/node_modules/@oxygen/shared/dist/crm-activity-events.js +1 -0
  43. package/node_modules/@oxygen/shared/dist/crm-record-brief.d.ts +10 -0
  44. package/node_modules/@oxygen/shared/dist/crm-record-brief.js +22 -0
  45. package/node_modules/@oxygen/shared/dist/deliverability-blacklist.d.ts +74 -0
  46. package/node_modules/@oxygen/shared/dist/deliverability-blacklist.js +118 -0
  47. package/node_modules/@oxygen/shared/dist/deliverability-checks.d.ts +138 -0
  48. package/node_modules/@oxygen/shared/dist/deliverability-checks.js +124 -0
  49. package/node_modules/@oxygen/shared/dist/deliverability-evidence.d.ts +366 -0
  50. package/node_modules/@oxygen/shared/dist/deliverability-evidence.js +169 -0
  51. package/node_modules/@oxygen/shared/dist/deliverability-loop.d.ts +334 -0
  52. package/node_modules/@oxygen/shared/dist/deliverability-loop.js +312 -0
  53. package/node_modules/@oxygen/shared/dist/deliverability-surface.d.ts +42 -0
  54. package/node_modules/@oxygen/shared/dist/deliverability-surface.js +66 -0
  55. package/node_modules/@oxygen/shared/dist/diagnostic-spool.js +34 -4
  56. package/node_modules/@oxygen/shared/dist/domain-authentication.d.ts +24 -0
  57. package/node_modules/@oxygen/shared/dist/domain-authentication.js +20 -0
  58. package/node_modules/@oxygen/shared/dist/file-import.js +11 -3
  59. package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.d.ts +16 -0
  60. package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.js +30 -2
  61. package/node_modules/@oxygen/shared/dist/network-mining.d.ts +5 -0
  62. package/node_modules/@oxygen/shared/dist/network-mining.js +9 -0
  63. package/node_modules/@oxygen/shared/dist/received-authentication.d.ts +74 -0
  64. package/node_modules/@oxygen/shared/dist/received-authentication.js +452 -0
  65. package/node_modules/@oxygen/shared/dist/recipient-provider.d.ts +21 -0
  66. package/node_modules/@oxygen/shared/dist/recipient-provider.js +33 -0
  67. package/node_modules/@oxygen/shared/dist/reply-oxygent.d.ts +73 -0
  68. package/node_modules/@oxygen/shared/dist/reply-oxygent.js +31 -0
  69. package/node_modules/@oxygen/shared/dist/repricing.d.ts +10 -1
  70. package/node_modules/@oxygen/shared/dist/repricing.js +16 -4
  71. package/node_modules/@oxygen/shared/dist/research-findings.d.ts +80 -0
  72. package/node_modules/@oxygen/shared/dist/research-findings.js +199 -0
  73. package/node_modules/@oxygen/shared/dist/sequence-internals/esp-policy.d.ts +2 -0
  74. package/node_modules/@oxygen/shared/dist/sequence-internals/esp-policy.js +4 -0
  75. package/node_modules/@oxygen/shared/dist/sequences.d.ts +6 -0
  76. package/node_modules/@oxygen/shared/dist/sequences.js +8 -0
  77. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +20 -3
  78. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  79. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  80. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.d.ts +8 -2
  81. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +63 -0
  82. package/package.json +1 -1
@@ -0,0 +1,334 @@
1
+ import type { DeliverabilityFindingStatus, DeliverabilityRunStatus, PlacementReceiverProvider } from "./deliverability-checks.js";
2
+ import { type CampaignOutcomes, type DeliverabilityCheckState, type PlacementEvidence } from "./deliverability-evidence.js";
3
+ /**
4
+ * The deliverability evidence loop: a scheduled Workflow that reads the stored deliverability evidence
5
+ * (copy scores, placement observations, DNS/BIMI/tracking/blacklist findings, campaign outcomes) and
6
+ * turns threshold crossings into proposals. This module is the whole policy: pure, deterministic and
7
+ * free of I/O. It never sends email, never starts a copy test and never changes a sender; every proposal
8
+ * only recommends an action, and that action keeps its own preview and approval when a person takes it.
9
+ * Proposals are stored with the run (the review step's output), so every daily run completes and the
10
+ * next one reads what earlier runs raised.
11
+ */
12
+ export declare const DELIVERABILITY_LOOP_TEMPLATE_ID = "deliverability-evidence-loop";
13
+ /** The loop's two Workflow tools. `@oxygen/tools` registers these exact ids; the cooldown read finds earlier reviews by the second. */
14
+ export declare const DELIVERABILITY_LOOP_COPY_SCORE_TOOL_ID = "oxygen.deliverability_copy_score";
15
+ export declare const DELIVERABILITY_LOOP_REVIEW_TOOL_ID = "oxygen.deliverability_evidence_review";
16
+ export type DeliverabilityLoopThresholds = {
17
+ /** Days in the current window; the previous window is the same length, immediately before it. */
18
+ windowDays: number;
19
+ /** Settled probes each window needs before an inbox-rate change is trusted. */
20
+ minSample: number;
21
+ /** Absolute inbox-rate fall (0.25 = 25 percentage points) that raises a proposal. */
22
+ inboxDrop: number;
23
+ /** Active copy variants scored and reviewed per cycle. */
24
+ maxCopies: number;
25
+ /** Sending domains reviewed per cycle. */
26
+ maxDomains: number;
27
+ /** A proposal an earlier live run of this Workflow raised is not raised again for this many days. */
28
+ cooldownDays: number;
29
+ };
30
+ export declare const DELIVERABILITY_LOOP_DEFAULTS: DeliverabilityLoopThresholds;
31
+ /** Inclusive bounds per threshold; values outside them are refused, never clamped silently. */
32
+ export declare const DELIVERABILITY_LOOP_LIMITS: Record<keyof DeliverabilityLoopThresholds, {
33
+ min: number;
34
+ max: number;
35
+ integer: boolean;
36
+ }>;
37
+ /** Request field per threshold (Workflow node params are snake_case). */
38
+ export declare const DELIVERABILITY_LOOP_PARAM_NAMES: Record<keyof DeliverabilityLoopThresholds, string>;
39
+ /** A workspace copy test holds at most this many messages (senders × receivers × variants). */
40
+ export declare const DELIVERABILITY_LOOP_COPY_TEST_MAX_MESSAGES = 50;
41
+ /** Receiver providers a workspace copy test can reach: this workspace's connected Google and Microsoft inboxes. */
42
+ export declare const DELIVERABILITY_LOOP_COPY_TEST_PROVIDERS: readonly ["google", "microsoft"];
43
+ export type DeliverabilityLoopCopyTestProvider = (typeof DELIVERABILITY_LOOP_COPY_TEST_PROVIDERS)[number];
44
+ export type DeliverabilityLoopThresholdIssue = {
45
+ field: string;
46
+ message: string;
47
+ };
48
+ /** Defaults under the supplied values; every supplied value must be a number inside its bounds. */
49
+ export declare function resolveDeliverabilityLoopThresholds(input: Record<string, unknown>): {
50
+ thresholds: DeliverabilityLoopThresholds;
51
+ issues: DeliverabilityLoopThresholdIssue[];
52
+ };
53
+ /** One active email Sequence variant and its latest stored content score. */
54
+ export type DeliverabilityLoopCopy = {
55
+ sequenceId: string;
56
+ sequenceName: string;
57
+ stepId: string;
58
+ stepIndex: number;
59
+ /** "A" is the step's base copy; "B", "C", … its variants (the workspace copy-test keys). */
60
+ variantKey: string;
61
+ fromAddress: string | null;
62
+ /**
63
+ * Domains this copy is sent from: the step's pinned inbox or sender profile, else the Sequence's
64
+ * sending pool. Null when the Sequence may send from any workspace inbox across several domains,
65
+ * so the copy cannot be tied to one domain.
66
+ */
67
+ senderDomains: string[] | null;
68
+ subject: string;
69
+ contentHash: string;
70
+ score: {
71
+ /** The newest stored content-score run with a score; null when this copy was never scored. */
72
+ runId: string | null;
73
+ scoredAt: string | null;
74
+ value: number | null;
75
+ requiredScore: number | null;
76
+ verdict: DeliverabilityFindingStatus;
77
+ /** Triggered rules that count against the copy, highest first. */
78
+ topRules: string[];
79
+ /** Status of the newest attempt when it is newer than the score above (for example a failed rescoring). */
80
+ latestAttempt: DeliverabilityRunStatus | null;
81
+ };
82
+ };
83
+ export type DeliverabilityLoopWindowEvidence = {
84
+ placement: PlacementEvidence;
85
+ byReceiverProvider: Array<{
86
+ receiverProvider: PlacementReceiverProvider;
87
+ placement: PlacementEvidence;
88
+ }>;
89
+ campaign: CampaignOutcomes | null;
90
+ /** Copy hashes probed from this domain in the window, most probes first. */
91
+ contentHashes: string[];
92
+ };
93
+ export type DeliverabilityLoopDomain = {
94
+ domain: string;
95
+ current: DeliverabilityLoopWindowEvidence;
96
+ previous: DeliverabilityLoopWindowEvidence;
97
+ /** Latest dns_auth, bimi and blacklist state of the domain. */
98
+ checks: DeliverabilityCheckState[];
99
+ };
100
+ export type DeliverabilityLoopTrackingHost = {
101
+ host: string;
102
+ status: string;
103
+ state: DeliverabilityCheckState;
104
+ };
105
+ export type DeliverabilityLoopListing = {
106
+ subject: string;
107
+ check: string;
108
+ zone: string;
109
+ listedAt: string;
110
+ runId: string;
111
+ };
112
+ export type DeliverabilityLoopSnapshot = {
113
+ now: string;
114
+ window: {
115
+ currentSince: string;
116
+ previousSince: string;
117
+ until: string;
118
+ };
119
+ copies: DeliverabilityLoopCopy[];
120
+ copiesTotal: number;
121
+ domains: DeliverabilityLoopDomain[];
122
+ domainsTotal: number;
123
+ tracking: DeliverabilityLoopTrackingHost[];
124
+ /** Listings that started in the current window (the previous answer for that zone was not listed, or there was none). */
125
+ newListings: DeliverabilityLoopListing[];
126
+ /**
127
+ * Subjects listed on at least one zone by the latest ANSWERED (pass or fail) result for that zone, so
128
+ * a later refused or timed-out check never hides a listing. `runId`/`checkedAt` name the newest run
129
+ * that answered a listed zone.
130
+ */
131
+ activeListings: Array<{
132
+ subject: string;
133
+ checks: string[];
134
+ runId: string;
135
+ checkedAt: string;
136
+ }>;
137
+ /** Subjects of every kind (sending, tracking and link domains) whose newest blacklist run got no usable answer. */
138
+ unansweredBlacklist: Array<{
139
+ subject: string;
140
+ state: DeliverabilityRunStatus;
141
+ runId: string;
142
+ checkedAt: string;
143
+ }>;
144
+ /** Proposal fingerprints earlier live runs of this Workflow raised within the cooldown. */
145
+ recentFingerprints: string[];
146
+ };
147
+ export declare const DELIVERABILITY_LOOP_PROPOSAL_KINDS: readonly ["placement_drop", "dns_auth_failure", "tracking_dns_failure", "new_listing", "high_content_score"];
148
+ export type DeliverabilityLoopProposalKind = (typeof DELIVERABILITY_LOOP_PROPOSAL_KINDS)[number];
149
+ type CopyRef = {
150
+ sequence_id: string;
151
+ sequence_name: string;
152
+ step_id: string;
153
+ step_index: number;
154
+ variant_key: string;
155
+ subject: string;
156
+ content_hash: string;
157
+ };
158
+ /**
159
+ * How big a recommended workspace copy test is. The test sends one message per sender × receiver ×
160
+ * variant, so the person chooses senders and receivers to reach `min_messages`; it never exceeds
161
+ * `max_messages`.
162
+ */
163
+ export type DeliverabilityLoopCopyTestSize = {
164
+ variants: number;
165
+ min_messages: number;
166
+ max_messages: number;
167
+ messages: "senders × receivers × variants";
168
+ };
169
+ export type DeliverabilityLoopAction = {
170
+ type: "run_workspace_copy_test";
171
+ /** Never started by this Workflow; a person previews and approves it. */
172
+ requires_human_approval: true;
173
+ copy: CopyRef | null;
174
+ variants: string[];
175
+ sender_domain: string;
176
+ receiver_provider: DeliverabilityLoopCopyTestProvider;
177
+ size: DeliverabilityLoopCopyTestSize;
178
+ next_steps: string[];
179
+ } | {
180
+ /** A drop at a receiver no workspace copy test reaches (Yahoo, other providers). */
181
+ type: "review_placement";
182
+ requires_human_approval: true;
183
+ sender_domain: string;
184
+ receiver_provider: PlacementReceiverProvider;
185
+ next_steps: string[];
186
+ } | {
187
+ type: "fix_dns";
188
+ requires_human_approval: true;
189
+ domain: string;
190
+ failing_checks: string[];
191
+ next_steps: string[];
192
+ } | {
193
+ type: "fix_tracking_host";
194
+ requires_human_approval: true;
195
+ host: string;
196
+ failing_checks: string[];
197
+ next_steps: string[];
198
+ } | {
199
+ type: "investigate_listing";
200
+ requires_human_approval: true;
201
+ subject: string;
202
+ checks: string[];
203
+ next_steps: string[];
204
+ } | {
205
+ type: "revise_copy";
206
+ requires_human_approval: true;
207
+ copy: CopyRef;
208
+ copy_test: {
209
+ variants: string[];
210
+ size: DeliverabilityLoopCopyTestSize;
211
+ };
212
+ next_steps: string[];
213
+ };
214
+ export type DeliverabilityLoopProposal = {
215
+ fingerprint: string;
216
+ kind: DeliverabilityLoopProposalKind;
217
+ severity: "high" | "medium";
218
+ title: string;
219
+ evidence: Record<string, unknown>;
220
+ recommended_action: DeliverabilityLoopAction;
221
+ };
222
+ export type DeliverabilityLoopPlacementPair = {
223
+ sender_domain: string;
224
+ receiver_provider: PlacementReceiverProvider;
225
+ previous: {
226
+ settled: number;
227
+ inbox_rate: number | null;
228
+ };
229
+ current: {
230
+ settled: number;
231
+ inbox_rate: number | null;
232
+ };
233
+ delta: number | null;
234
+ verdict: "drop" | "steady" | "low_sample";
235
+ };
236
+ export type DeliverabilityLoopReport = {
237
+ status: "clear" | "attention";
238
+ summary: string;
239
+ window: {
240
+ current_since: string;
241
+ previous_since: string;
242
+ until: string;
243
+ days: number;
244
+ };
245
+ thresholds: {
246
+ window_days: number;
247
+ min_sample: number;
248
+ inbox_drop: number;
249
+ max_copies: number;
250
+ max_domains: number;
251
+ cooldown_days: number;
252
+ };
253
+ placement: {
254
+ pairs: DeliverabilityLoopPlacementPair[];
255
+ drops: number;
256
+ low_sample: number;
257
+ };
258
+ campaign: Array<{
259
+ sender_domain: string;
260
+ current: CampaignOutcomes | null;
261
+ previous: CampaignOutcomes | null;
262
+ }>;
263
+ domains: {
264
+ reviewed: number;
265
+ total: number;
266
+ truncated: boolean;
267
+ checks: Array<{
268
+ domain: string;
269
+ dns_auth: string;
270
+ bimi: string;
271
+ blacklist: string;
272
+ failing: string[];
273
+ warning: string[];
274
+ }>;
275
+ };
276
+ tracking: Array<{
277
+ host: string;
278
+ status: string;
279
+ check: string;
280
+ failing: string[];
281
+ }>;
282
+ listings: {
283
+ new: DeliverabilityLoopListing[];
284
+ active: Array<{
285
+ subject: string;
286
+ checks: string[];
287
+ checked_at: string;
288
+ }>;
289
+ unanswered: Array<{
290
+ subject: string;
291
+ state: DeliverabilityRunStatus;
292
+ checked_at: string;
293
+ }>;
294
+ };
295
+ copy: {
296
+ reviewed: number;
297
+ total: number;
298
+ truncated: boolean;
299
+ verdicts: Record<DeliverabilityFindingStatus, number>;
300
+ items: Array<{
301
+ sequence_id: string;
302
+ sequence_name: string;
303
+ step_index: number;
304
+ variant_key: string;
305
+ content_hash: string;
306
+ score: number | null;
307
+ required_score: number | null;
308
+ verdict: DeliverabilityFindingStatus;
309
+ scored_at: string | null;
310
+ }>;
311
+ };
312
+ evidence_gaps: string[];
313
+ proposals: DeliverabilityLoopProposal[];
314
+ proposal_count: number;
315
+ proposal_fingerprints: string[];
316
+ /** Crossed thresholds already raised by this Workflow within the cooldown; listed, not raised again. */
317
+ suppressed_proposals: Array<{
318
+ fingerprint: string;
319
+ kind: DeliverabilityLoopProposalKind;
320
+ title: string;
321
+ }>;
322
+ /** What this Workflow never does, stated on every run. */
323
+ effects: {
324
+ sends_email: false;
325
+ starts_copy_test: false;
326
+ changes_senders: false;
327
+ };
328
+ };
329
+ /**
330
+ * Turn one evidence snapshot into the run's report: a concise summary, the evidence behind it and a
331
+ * proposal per crossed threshold. Deterministic for a given snapshot and thresholds.
332
+ */
333
+ export declare function evaluateDeliverabilityLoop(snapshot: DeliverabilityLoopSnapshot, t?: DeliverabilityLoopThresholds): DeliverabilityLoopReport;
334
+ export {};
@@ -0,0 +1,312 @@
1
+ import { PLACEMENT_SIGNAL_INBOX_DROP, PLACEMENT_SIGNAL_MIN_SAMPLE, } from "./deliverability-evidence.js";
2
+ /**
3
+ * The deliverability evidence loop: a scheduled Workflow that reads the stored deliverability evidence
4
+ * (copy scores, placement observations, DNS/BIMI/tracking/blacklist findings, campaign outcomes) and
5
+ * turns threshold crossings into proposals. This module is the whole policy: pure, deterministic and
6
+ * free of I/O. It never sends email, never starts a copy test and never changes a sender; every proposal
7
+ * only recommends an action, and that action keeps its own preview and approval when a person takes it.
8
+ * Proposals are stored with the run (the review step's output), so every daily run completes and the
9
+ * next one reads what earlier runs raised.
10
+ */
11
+ export const DELIVERABILITY_LOOP_TEMPLATE_ID = "deliverability-evidence-loop";
12
+ /** The loop's two Workflow tools. `@oxygen/tools` registers these exact ids; the cooldown read finds earlier reviews by the second. */
13
+ export const DELIVERABILITY_LOOP_COPY_SCORE_TOOL_ID = "oxygen.deliverability_copy_score";
14
+ export const DELIVERABILITY_LOOP_REVIEW_TOOL_ID = "oxygen.deliverability_evidence_review";
15
+ export const DELIVERABILITY_LOOP_DEFAULTS = {
16
+ windowDays: 7,
17
+ minSample: PLACEMENT_SIGNAL_MIN_SAMPLE,
18
+ inboxDrop: PLACEMENT_SIGNAL_INBOX_DROP,
19
+ maxCopies: 20,
20
+ maxDomains: 20,
21
+ cooldownDays: 7,
22
+ };
23
+ /** Inclusive bounds per threshold; values outside them are refused, never clamped silently. */
24
+ export const DELIVERABILITY_LOOP_LIMITS = {
25
+ windowDays: { min: 3, max: 14, integer: true },
26
+ minSample: { min: 4, max: 200, integer: true },
27
+ inboxDrop: { min: 0.05, max: 0.9, integer: false },
28
+ // Bounded so a cycle of unresponsive scorer calls (10 s each, 4 at a time) stays inside one Workflow step's time budget.
29
+ maxCopies: { min: 1, max: 30, integer: true },
30
+ maxDomains: { min: 1, max: 50, integer: true },
31
+ cooldownDays: { min: 1, max: 30, integer: true },
32
+ };
33
+ /** Request field per threshold (Workflow node params are snake_case). */
34
+ export const DELIVERABILITY_LOOP_PARAM_NAMES = {
35
+ windowDays: "window_days",
36
+ minSample: "min_sample",
37
+ inboxDrop: "inbox_drop",
38
+ maxCopies: "max_copies",
39
+ maxDomains: "max_domains",
40
+ cooldownDays: "cooldown_days",
41
+ };
42
+ /** A workspace copy test holds at most this many messages (senders × receivers × variants). */
43
+ export const DELIVERABILITY_LOOP_COPY_TEST_MAX_MESSAGES = 50;
44
+ /** Receiver providers a workspace copy test can reach: this workspace's connected Google and Microsoft inboxes. */
45
+ export const DELIVERABILITY_LOOP_COPY_TEST_PROVIDERS = ["google", "microsoft"];
46
+ /** Defaults under the supplied values; every supplied value must be a number inside its bounds. */
47
+ export function resolveDeliverabilityLoopThresholds(input) {
48
+ const thresholds = { ...DELIVERABILITY_LOOP_DEFAULTS };
49
+ const issues = [];
50
+ for (const key of Object.keys(DELIVERABILITY_LOOP_DEFAULTS)) {
51
+ const field = DELIVERABILITY_LOOP_PARAM_NAMES[key];
52
+ const value = input[field];
53
+ if (value === undefined || value === null)
54
+ continue;
55
+ const limit = DELIVERABILITY_LOOP_LIMITS[key];
56
+ if (typeof value !== "number" || !Number.isFinite(value) || value < limit.min || value > limit.max || (limit.integer && !Number.isInteger(value))) {
57
+ issues.push({ field, message: `${field} must be ${limit.integer ? "an integer" : "a number"} from ${limit.min} to ${limit.max}.` });
58
+ continue;
59
+ }
60
+ thresholds[key] = value;
61
+ }
62
+ return { thresholds, issues };
63
+ }
64
+ export const DELIVERABILITY_LOOP_PROPOSAL_KINDS = ["placement_drop", "dns_auth_failure", "tracking_dns_failure", "new_listing", "high_content_score"];
65
+ const PROVIDER_LABEL = { google: "Google", microsoft: "Microsoft", yahoo: "Yahoo", other: "other providers", unknown: "unknown providers" };
66
+ const percent = (value) => (value === null ? "n/a" : `${Math.round(value * 100)}%`);
67
+ const round4 = (value) => Math.round(value * 10_000) / 10_000;
68
+ const plural = (count, one, many = `${one}s`) => `${count} ${count === 1 ? one : many}`;
69
+ const copyRef = (copy) => ({
70
+ sequence_id: copy.sequenceId, sequence_name: copy.sequenceName, step_id: copy.stepId, step_index: copy.stepIndex,
71
+ variant_key: copy.variantKey, subject: copy.subject, content_hash: copy.contentHash,
72
+ });
73
+ const copyTestCommand = (copy) => `oxygen deliverability workspace-test copy ${copy.sequence_id} --step-index ${copy.step_index} --json`;
74
+ const copyTestSteps = (copy) => [
75
+ ...(copy ? [`Read the exact copy: ${copyTestCommand(copy)}`] : ["Choose the copy to test; no active Sequence copy is known to be sent from this domain."]),
76
+ "Preview the workspace copy test (sends nothing, 0 credits), review every sender, receiver and copy, then approve it explicitly.",
77
+ ];
78
+ const copyTestSize = (variants, minMessages) => ({
79
+ variants, min_messages: Math.min(minMessages, DELIVERABILITY_LOOP_COPY_TEST_MAX_MESSAGES),
80
+ max_messages: DELIVERABILITY_LOOP_COPY_TEST_MAX_MESSAGES, messages: "senders × receivers × variants",
81
+ });
82
+ const sizeText = (size) => `senders × receivers × ${plural(size.variants, "variant")} reaches at least ${size.min_messages} messages (one test sends at most ${size.max_messages})`;
83
+ const testableProvider = (provider) => DELIVERABILITY_LOOP_COPY_TEST_PROVIDERS.includes(provider);
84
+ /**
85
+ * The copy most likely behind a domain's placement: probed from it in the window, else active copy whose
86
+ * sender is known to be on this domain. Copy that may go out from any workspace inbox is never guessed.
87
+ */
88
+ function copyForDomain(copies, domain) {
89
+ for (const hash of domain.current.contentHashes) {
90
+ const probed = copies.find((copy) => copy.contentHash === hash);
91
+ if (probed)
92
+ return probed;
93
+ }
94
+ return copies.find((copy) => copy.senderDomains?.includes(domain.domain)) ?? null;
95
+ }
96
+ function placementPairs(domain, t) {
97
+ const providers = new Set();
98
+ for (const entry of [...domain.previous.byReceiverProvider, ...domain.current.byReceiverProvider]) {
99
+ if (entry.receiverProvider !== "unknown")
100
+ providers.add(entry.receiverProvider);
101
+ }
102
+ return [...providers].sort().map((provider) => {
103
+ const before = domain.previous.byReceiverProvider.find((entry) => entry.receiverProvider === provider)?.placement;
104
+ const now = domain.current.byReceiverProvider.find((entry) => entry.receiverProvider === provider)?.placement;
105
+ const previous = { settled: before?.settled ?? 0, inbox_rate: before?.inboxRate.value ?? null };
106
+ const current = { settled: now?.settled ?? 0, inbox_rate: now?.inboxRate.value ?? null };
107
+ const sampled = previous.settled >= t.minSample && current.settled >= t.minSample;
108
+ const delta = previous.inbox_rate !== null && current.inbox_rate !== null ? round4(current.inbox_rate - previous.inbox_rate) : null;
109
+ // Rates are rounded to 4 places, so a threshold met exactly must not miss on float noise.
110
+ const verdict = !sampled ? "low_sample" : delta !== null && -delta >= t.inboxDrop - 1e-9 ? "drop" : "steady";
111
+ return { sender_domain: domain.domain, receiver_provider: provider, previous, current, delta, verdict };
112
+ });
113
+ }
114
+ const stateLabel = (state) => state?.state ?? "never_run";
115
+ const blacklistGap = (subject) => `${subject}: the latest blacklist check got no usable answer; its listing state is unknown, not clean.`;
116
+ /**
117
+ * Turn one evidence snapshot into the run's report: a concise summary, the evidence behind it and a
118
+ * proposal per crossed threshold. Deterministic for a given snapshot and thresholds.
119
+ */
120
+ export function evaluateDeliverabilityLoop(snapshot, t = DELIVERABILITY_LOOP_DEFAULTS) {
121
+ const crossed = [];
122
+ const gaps = [];
123
+ const pairs = snapshot.domains.flatMap((domain) => placementPairs(domain, t));
124
+ for (const pair of pairs.filter((p) => p.verdict === "drop")) {
125
+ const domain = snapshot.domains.find((d) => d.domain === pair.sender_domain);
126
+ const label = PROVIDER_LABEL[pair.receiver_provider];
127
+ const provider = pair.receiver_provider;
128
+ let action;
129
+ if (testableProvider(provider)) {
130
+ const copy = copyForDomain(snapshot.copies, domain);
131
+ const ref = copy ? copyRef(copy) : null;
132
+ const size = copyTestSize(1, t.minSample);
133
+ action = {
134
+ type: "run_workspace_copy_test", requires_human_approval: true, copy: ref, variants: ref ? [ref.variant_key] : [],
135
+ sender_domain: pair.sender_domain, receiver_provider: provider, size,
136
+ next_steps: [
137
+ ...copyTestSteps(ref),
138
+ `Choose senders on ${pair.sender_domain} and connected ${label} receivers so ${sizeText(size)}, the minimum sample this drop was measured on.`,
139
+ "Placement is directional evidence: do not pause or change senders on it alone.",
140
+ ],
141
+ };
142
+ }
143
+ else {
144
+ action = {
145
+ type: "review_placement", requires_human_approval: true, sender_domain: pair.sender_domain, receiver_provider: provider,
146
+ next_steps: [
147
+ `A workspace copy test reaches only this workspace's connected Google and Microsoft receivers, so it cannot test ${label}.`,
148
+ `Compare this run's placement evidence for ${pair.sender_domain} with its recent sends, copy changes and blacklist findings.`,
149
+ "Placement is directional evidence: do not pause or change senders on it alone.",
150
+ ],
151
+ };
152
+ }
153
+ crossed.push({
154
+ fingerprint: `placement_drop:${pair.sender_domain}:${pair.receiver_provider}`,
155
+ kind: "placement_drop",
156
+ severity: "high",
157
+ title: `Inbox rate for ${pair.sender_domain} at ${label} fell from ${percent(pair.previous.inbox_rate)} to ${percent(pair.current.inbox_rate)}`,
158
+ evidence: { ...pair, min_sample: t.minSample, threshold: t.inboxDrop, denominator_of: "settled_probes" },
159
+ recommended_action: action,
160
+ });
161
+ }
162
+ const lowSample = pairs.filter((p) => p.verdict === "low_sample").length;
163
+ if (!pairs.some((p) => p.previous.settled + p.current.settled > 0))
164
+ gaps.push("No placement observations in either window; run an approved copy test to collect some.");
165
+ const domainChecks = snapshot.domains.map((domain) => {
166
+ const state = (kind) => domain.checks.find((check) => check.kind === kind);
167
+ const dns = state("dns_auth");
168
+ const failing = domain.checks.flatMap((check) => check.failing);
169
+ const warning = domain.checks.flatMap((check) => check.warning);
170
+ const dnsFailing = dns?.failing ?? [];
171
+ if (dnsFailing.length) {
172
+ crossed.push({
173
+ fingerprint: `dns_auth_failure:${domain.domain}:${[...dnsFailing].sort().join(",")}`,
174
+ kind: "dns_auth_failure",
175
+ severity: "high",
176
+ title: `${domain.domain} fails ${plural(dnsFailing.length, "DNS authentication check")}`,
177
+ evidence: { domain: domain.domain, failing_checks: dnsFailing, run_id: dns.runId, checked_at: dns.startedAt },
178
+ recommended_action: {
179
+ type: "fix_dns", requires_human_approval: true, domain: domain.domain, failing_checks: dnsFailing,
180
+ next_steps: [
181
+ `Read the saved evidence behind this proposal: oxygen domains dns ${domain.domain} --cached --json`,
182
+ `Check the live records again before changing anything: oxygen domains dns ${domain.domain} --deep --json`,
183
+ "Correct the records at the DNS host; the daily check records the fix.",
184
+ ],
185
+ },
186
+ });
187
+ }
188
+ if (!dns || dns.state === "never_run")
189
+ gaps.push(`${domain.domain}: DNS authentication has not been checked yet.`);
190
+ else if (dns.state === "failed" || dns.state === "unknown")
191
+ gaps.push(`${domain.domain}: the latest DNS authentication check could not read public DNS.`);
192
+ const blacklist = state("blacklist");
193
+ if (!blacklist || blacklist.state === "never_run")
194
+ gaps.push(`${domain.domain}: no blacklist check has run.`);
195
+ else if (blacklist.state === "unknown" || blacklist.state === "failed")
196
+ gaps.push(blacklistGap(domain.domain));
197
+ return { domain: domain.domain, dns_auth: stateLabel(dns), bimi: stateLabel(state("bimi")), blacklist: stateLabel(blacklist), failing, warning };
198
+ });
199
+ const tracking = snapshot.tracking.map((host) => {
200
+ if (host.state.failing.length) {
201
+ crossed.push({
202
+ fingerprint: `tracking_dns_failure:${host.host}:${[...host.state.failing].sort().join(",")}`,
203
+ kind: "tracking_dns_failure",
204
+ severity: "medium",
205
+ title: `Tracking host ${host.host} fails ${plural(host.state.failing.length, "check")}`,
206
+ evidence: { host: host.host, status: host.status, failing_checks: host.state.failing, run_id: host.state.runId, checked_at: host.state.startedAt },
207
+ recommended_action: {
208
+ type: "fix_tracking_host", requires_human_approval: true, host: host.host, failing_checks: host.state.failing,
209
+ next_steps: ["Point the host's CNAME at the tracking target shown in Settings, then wait for the daily verification."],
210
+ },
211
+ });
212
+ }
213
+ return { host: host.host, status: host.status, check: host.state.state, failing: host.state.failing };
214
+ });
215
+ for (const subject of snapshot.unansweredBlacklist)
216
+ gaps.push(blacklistGap(subject.subject));
217
+ const listingsBySubject = new Map();
218
+ for (const listing of snapshot.newListings)
219
+ listingsBySubject.set(listing.subject, [...(listingsBySubject.get(listing.subject) ?? []), listing]);
220
+ for (const [subject, listings] of [...listingsBySubject].sort(([a], [b]) => a.localeCompare(b))) {
221
+ const checks = [...new Set(listings.map((listing) => listing.check))].sort();
222
+ crossed.push({
223
+ fingerprint: `new_listing:${subject}:${checks.join(",")}`,
224
+ kind: "new_listing",
225
+ severity: "high",
226
+ title: `${subject} was newly listed on ${plural(checks.length, "blacklist zone")}`,
227
+ evidence: { subject, listings },
228
+ recommended_action: {
229
+ type: "investigate_listing", requires_human_approval: true, subject, checks,
230
+ next_steps: [
231
+ "Compare the listing time with recent sends, new link domains and copy changes from this domain.",
232
+ "Request delisting from the zone operator once the cause is fixed; the daily check records the delisting.",
233
+ ],
234
+ },
235
+ });
236
+ }
237
+ const verdicts = { pass: 0, warn: 0, fail: 0, unknown: 0 };
238
+ const proposedCopy = new Set();
239
+ for (const copy of snapshot.copies) {
240
+ verdicts[copy.score.verdict] += 1;
241
+ // Identical copy in several Sequences is one content hash, so one proposal (naming its first use).
242
+ if (copy.score.verdict !== "fail" || proposedCopy.has(copy.contentHash))
243
+ continue;
244
+ proposedCopy.add(copy.contentHash);
245
+ const ref = copyRef(copy);
246
+ const size = copyTestSize(2, 2 * t.minSample);
247
+ crossed.push({
248
+ fingerprint: `high_content_score:${copy.contentHash}`,
249
+ kind: "high_content_score",
250
+ severity: "medium",
251
+ title: `"${copy.sequenceName}" step ${copy.stepIndex + 1} variant ${copy.variantKey} scores ${copy.score.value ?? "n/a"} of ${copy.score.requiredScore ?? "n/a"}`,
252
+ evidence: { ...ref, score: copy.score.value, required_score: copy.score.requiredScore, top_rules: copy.score.topRules, run_id: copy.score.runId, scored_at: copy.score.scoredAt },
253
+ recommended_action: {
254
+ type: "revise_copy", requires_human_approval: true, copy: ref,
255
+ copy_test: { variants: [ref.variant_key, "revised"], size },
256
+ next_steps: [
257
+ copy.score.topRules.length ? `Revise the copy that triggers ${copy.score.topRules.slice(0, 3).join(", ")}.` : "Revise the copy, then score it again.",
258
+ `Optionally compare the current and revised copy in one approved workspace copy test where ${sizeText(size)}: ${copyTestCommand(ref)}`,
259
+ ],
260
+ },
261
+ });
262
+ }
263
+ if (snapshot.copies.length && snapshot.copies.every((copy) => copy.score.runId === null))
264
+ gaps.push("No active copy has a stored content score; copy scoring may be unavailable.");
265
+ else if (snapshot.copies.some((copy) => copy.score.latestAttempt === "failed"))
266
+ gaps.push("Copy scoring failed for some copy this cycle; the newest earlier score is shown.");
267
+ const recent = new Set(snapshot.recentFingerprints);
268
+ const proposals = crossed.filter((proposal) => !recent.has(proposal.fingerprint));
269
+ const suppressed = crossed.filter((proposal) => recent.has(proposal.fingerprint)).map(({ fingerprint, kind, title }) => ({ fingerprint, kind, title }));
270
+ const drops = pairs.filter((p) => p.verdict === "drop");
271
+ const dnsFailingDomains = domainChecks.filter((d) => snapshot.domains.find((x) => x.domain === d.domain).checks.some((c) => c.kind === "dns_auth" && c.failing.length));
272
+ const until = snapshot.window.until.slice(0, 10);
273
+ const lines = [
274
+ `Deliverability evidence for the ${t.windowDays} days to ${until}, compared with the ${t.windowDays} days before.`,
275
+ `Placement: ${plural(pairs.length, "sender domain × receiver pair")} compared; ${plural(drops.length, "inbox-rate drop")}${drops.length ? ` (${drops.map((p) => `${p.sender_domain} at ${PROVIDER_LABEL[p.receiver_provider]} ${percent(p.previous.inbox_rate)} → ${percent(p.current.inbox_rate)}, n=${p.previous.settled}/${p.current.settled}`).join("; ")})` : ""}; ${lowSample} below the ${t.minSample}-probe minimum.`,
276
+ `Authentication: ${dnsFailingDomains.length} of ${plural(snapshot.domains.length, "sending domain")} fail DNS checks${dnsFailingDomains.length ? ` (${dnsFailingDomains.map((d) => d.domain).join(", ")})` : ""}.`,
277
+ `Tracking: ${tracking.filter((h) => h.failing.length).length} of ${plural(tracking.length, "tracking host")} failing.`,
278
+ `Blacklists: ${plural(listingsBySubject.size, "subject")} newly listed; ${plural(snapshot.activeListings.length, "subject")} currently listed${snapshot.unansweredBlacklist.length ? `; ${plural(snapshot.unansweredBlacklist.length, "subject")} without a usable answer on the latest check` : ""}.`,
279
+ `Copy: ${plural(snapshot.copies.length, "active copy", "active copies")} reviewed; ${verdicts.fail} high, ${verdicts.warn} elevated, ${verdicts.unknown} unscored.`,
280
+ proposals.length
281
+ ? `${plural(proposals.length, "new proposal")}${suppressed.length ? `; ${suppressed.length} already raised within ${t.cooldownDays} days` : ""}. Each recommended action keeps its own preview and approval. Nothing was sent, tested or changed.`
282
+ : `No new proposal${suppressed.length ? ` (${suppressed.length} already raised within ${t.cooldownDays} days)` : ""}. Nothing was sent, tested or changed.`,
283
+ ];
284
+ return {
285
+ status: crossed.length ? "attention" : "clear",
286
+ summary: lines.join("\n"),
287
+ window: { current_since: snapshot.window.currentSince, previous_since: snapshot.window.previousSince, until: snapshot.window.until, days: t.windowDays },
288
+ thresholds: { window_days: t.windowDays, min_sample: t.minSample, inbox_drop: t.inboxDrop, max_copies: t.maxCopies, max_domains: t.maxDomains, cooldown_days: t.cooldownDays },
289
+ placement: { pairs, drops: drops.length, low_sample: lowSample },
290
+ campaign: snapshot.domains.map((domain) => ({ sender_domain: domain.domain, current: domain.current.campaign, previous: domain.previous.campaign })),
291
+ domains: { reviewed: snapshot.domains.length, total: snapshot.domainsTotal, truncated: snapshot.domainsTotal > snapshot.domains.length, checks: domainChecks },
292
+ tracking,
293
+ listings: {
294
+ new: snapshot.newListings,
295
+ active: snapshot.activeListings.map(({ subject, checks, checkedAt }) => ({ subject, checks, checked_at: checkedAt })),
296
+ unanswered: snapshot.unansweredBlacklist.map(({ subject, state, checkedAt }) => ({ subject, state, checked_at: checkedAt })),
297
+ },
298
+ copy: {
299
+ reviewed: snapshot.copies.length, total: snapshot.copiesTotal, truncated: snapshot.copiesTotal > snapshot.copies.length, verdicts,
300
+ items: snapshot.copies.map((copy) => ({
301
+ sequence_id: copy.sequenceId, sequence_name: copy.sequenceName, step_index: copy.stepIndex, variant_key: copy.variantKey, content_hash: copy.contentHash,
302
+ score: copy.score.value, required_score: copy.score.requiredScore, verdict: copy.score.verdict, scored_at: copy.score.scoredAt,
303
+ })),
304
+ },
305
+ evidence_gaps: [...new Set(gaps)],
306
+ proposals,
307
+ proposal_count: proposals.length,
308
+ proposal_fingerprints: proposals.map((proposal) => proposal.fingerprint),
309
+ suppressed_proposals: suppressed,
310
+ effects: { sends_email: false, starts_copy_test: false, changes_senders: false },
311
+ };
312
+ }