@oxygen-agent/cli 1.1014.1136 → 1.1014.1219
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/README.md +1 -1
- package/dist/command-manifest.js +2 -0
- package/dist/command-support.d.ts +8 -0
- package/dist/command-support.js +11 -0
- package/dist/commands/api-keys.d.ts +9 -0
- package/dist/commands/api-keys.js +4 -1
- package/dist/commands/capabilities.js +4 -2
- package/dist/commands/crm.js +28 -2
- package/dist/commands/dashboards.js +2 -2
- package/dist/commands/db.js +34 -0
- package/dist/commands/find.js +1 -1
- package/dist/commands/knowledge.js +2 -0
- package/dist/commands/mailboxes.js +3 -2
- package/dist/commands/messages.js +6 -1
- package/dist/commands/senders.js +2 -0
- package/dist/commands/sequences.js +1 -1
- package/dist/commands/signals.js +11 -2
- package/dist/deliverability-evidence-commands.d.ts +4 -0
- package/dist/deliverability-evidence-commands.js +104 -0
- package/dist/diagnostic-context.d.ts +21 -0
- package/dist/diagnostic-context.js +25 -0
- package/dist/diagnostics.d.ts +1 -3
- package/dist/diagnostics.js +12 -30
- package/dist/http-client.js +1 -1
- package/dist/network-mining-host.d.ts +6 -0
- package/dist/network-mining-host.js +10 -0
- package/node_modules/@oxygen/formula/dist/value-normalizers.d.ts +1 -1
- package/node_modules/@oxygen/formula/dist/value-normalizers.js +5 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +169 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.js +17 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +114 -5
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/operation-request.d.ts +92 -0
- package/node_modules/@oxygen/shared/dist/contracts/operation-request.js +36 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +3 -90
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +5 -39
- package/node_modules/@oxygen/shared/dist/contracts/reply-oxygent.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/reply-oxygent.js +123 -0
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +1 -0
- package/node_modules/@oxygen/shared/dist/crm-activity-events.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/crm-activity-events.js +1 -0
- package/node_modules/@oxygen/shared/dist/crm-record-brief.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/crm-record-brief.js +22 -0
- package/node_modules/@oxygen/shared/dist/deliverability-blacklist.d.ts +74 -0
- package/node_modules/@oxygen/shared/dist/deliverability-blacklist.js +118 -0
- package/node_modules/@oxygen/shared/dist/deliverability-checks.d.ts +138 -0
- package/node_modules/@oxygen/shared/dist/deliverability-checks.js +124 -0
- package/node_modules/@oxygen/shared/dist/deliverability-evidence.d.ts +366 -0
- package/node_modules/@oxygen/shared/dist/deliverability-evidence.js +169 -0
- package/node_modules/@oxygen/shared/dist/deliverability-loop.d.ts +334 -0
- package/node_modules/@oxygen/shared/dist/deliverability-loop.js +312 -0
- package/node_modules/@oxygen/shared/dist/deliverability-surface.d.ts +42 -0
- package/node_modules/@oxygen/shared/dist/deliverability-surface.js +66 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-spool.js +34 -4
- package/node_modules/@oxygen/shared/dist/domain-authentication.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/domain-authentication.js +20 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +11 -3
- package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.js +30 -2
- package/node_modules/@oxygen/shared/dist/network-mining.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/network-mining.js +9 -0
- package/node_modules/@oxygen/shared/dist/received-authentication.d.ts +74 -0
- package/node_modules/@oxygen/shared/dist/received-authentication.js +452 -0
- package/node_modules/@oxygen/shared/dist/recipient-provider.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/recipient-provider.js +33 -0
- package/node_modules/@oxygen/shared/dist/reply-oxygent.d.ts +73 -0
- package/node_modules/@oxygen/shared/dist/reply-oxygent.js +31 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +10 -1
- package/node_modules/@oxygen/shared/dist/repricing.js +16 -4
- package/node_modules/@oxygen/shared/dist/research-findings.d.ts +80 -0
- package/node_modules/@oxygen/shared/dist/research-findings.js +199 -0
- package/node_modules/@oxygen/shared/dist/sequence-internals/esp-policy.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/sequence-internals/esp-policy.js +4 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +8 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +20 -3
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/workspace-event-catalog.d.ts +8 -2
- package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +63 -0
- package/package.json +1 -1
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import type { DeliverabilityCheckKind, DeliverabilityFindingStatus, DeliverabilityRunStatus, PlacementFolder, PlacementObservation, PlacementObservationSource, PlacementReceiverProvider, PlacementTab } from "./deliverability-checks.js";
|
|
2
|
+
/**
|
|
3
|
+
* The deliverability learning loop's read contract: evidence over stored check runs, placement
|
|
4
|
+
* observations and real campaign outcomes. Every rate carries its numerator, denominator and what
|
|
5
|
+
* the denominator counts; an empty bucket is reported as zeros, never interpolated.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DELIVERABILITY_EVIDENCE_BUCKETS: readonly ["day", "week"];
|
|
8
|
+
export type DeliverabilityEvidenceBucketSize = (typeof DELIVERABILITY_EVIDENCE_BUCKETS)[number];
|
|
9
|
+
/** Window bound per bucket size; a week window covers the whole 400-day retention. */
|
|
10
|
+
export declare const DELIVERABILITY_EVIDENCE_MAX_BUCKETS: Record<DeliverabilityEvidenceBucketSize, number>;
|
|
11
|
+
/** Below this denominator a rate is still reported, but flagged as a low sample. */
|
|
12
|
+
export declare const DELIVERABILITY_EVIDENCE_MIN_SAMPLE = 8;
|
|
13
|
+
export declare const DELIVERABILITY_EVIDENCE_MAX_CONTENT_HASHES = 10;
|
|
14
|
+
export declare const DELIVERABILITY_EVIDENCE_MAX_CHANGES = 100;
|
|
15
|
+
export declare const DELIVERABILITY_EVIDENCE_MAX_SIGNALS = 50;
|
|
16
|
+
/** Domain-subject checks whose latest state and status changes the evidence reports. */
|
|
17
|
+
export declare const DELIVERABILITY_EVIDENCE_DOMAIN_CHECKS: readonly ["dns_auth", "bimi", "blacklist"];
|
|
18
|
+
/** Content-score findings name each triggered rule as `content.rule.<symbol>`. */
|
|
19
|
+
export declare const DELIVERABILITY_CONTENT_RULE_PREFIX = "content.rule.";
|
|
20
|
+
/** Placement time: when the probe was sent, else when it was read, else when it was stored. */
|
|
21
|
+
export declare const PLACEMENT_EVIDENCE_TIME = "sent_at, else observed_at, else recorded_at";
|
|
22
|
+
/**
|
|
23
|
+
* Placement-change Signals: a sender domain × receiver provider pair is compared over a rolling
|
|
24
|
+
* current window against the window before it. Placement is directional evidence only, so a
|
|
25
|
+
* transition emits a Signal and never pauses a sender.
|
|
26
|
+
*/
|
|
27
|
+
export declare const PLACEMENT_SIGNAL_CURRENT_WINDOW_DAYS = 7;
|
|
28
|
+
export declare const PLACEMENT_SIGNAL_BASELINE_WINDOW_DAYS = 28;
|
|
29
|
+
/** Settled probes (inbox, spam, other or not observed) each window needs before a rate is trusted. */
|
|
30
|
+
export declare const PLACEMENT_SIGNAL_MIN_SAMPLE = 8;
|
|
31
|
+
/** Absolute inbox-rate drop (0.25 = 25 percentage points) that counts as a transition. */
|
|
32
|
+
export declare const PLACEMENT_SIGNAL_INBOX_DROP = 0.25;
|
|
33
|
+
/** Gmail-tab-classified inbox probes each window needs before a tab shift is trusted. */
|
|
34
|
+
export declare const PLACEMENT_SIGNAL_MIN_TAB_SAMPLE = 6;
|
|
35
|
+
/** Primary share must fall, and Promotions share rise, by at least this much. */
|
|
36
|
+
export declare const PLACEMENT_SIGNAL_TAB_SHIFT = 0.25;
|
|
37
|
+
/** One Signal per domain × provider × transition within this many days. */
|
|
38
|
+
export declare const PLACEMENT_SIGNAL_COOLDOWN_DAYS = 7;
|
|
39
|
+
export declare const PLACEMENT_SIGNAL_MAX_PAIRS = 500;
|
|
40
|
+
export declare const PLACEMENT_SIGNAL_MAX_EMITS = 50;
|
|
41
|
+
export declare const PLACEMENT_SIGNAL_TYPE = "placement_changed";
|
|
42
|
+
export declare const PLACEMENT_SIGNAL_SOURCE_PROVIDER = "oxygen_deliverability";
|
|
43
|
+
export declare const PLACEMENT_SIGNAL_SCHEMA = "placement_changed.v1";
|
|
44
|
+
export declare const PLACEMENT_TRANSITIONS: readonly ["inbox_rate_drop", "primary_to_promotions"];
|
|
45
|
+
export type PlacementTransitionKind = (typeof PLACEMENT_TRANSITIONS)[number];
|
|
46
|
+
export declare const PLACEMENT_COUNT_OUTCOMES: readonly ["observed", "not_observed", "send_failed", "send_unknown", "read_blocked"];
|
|
47
|
+
export declare const PLACEMENT_COUNT_FOLDERS: readonly ["inbox", "spam", "other", "not_observed", "unknown"];
|
|
48
|
+
export declare const PLACEMENT_COUNT_TABS: readonly ["primary", "promotions", "social", "updates", "forums", "focused", "other", "unknown"];
|
|
49
|
+
/** Raw placement counts for one scope; `tabs` counts inbox observations only. */
|
|
50
|
+
export type PlacementCounts = {
|
|
51
|
+
probes: number;
|
|
52
|
+
outcomes: Record<PlacementObservation["outcome"], number>;
|
|
53
|
+
folders: Record<PlacementFolder, number>;
|
|
54
|
+
tabs: Record<PlacementTab, number>;
|
|
55
|
+
spf: {
|
|
56
|
+
known: number;
|
|
57
|
+
pass: number;
|
|
58
|
+
};
|
|
59
|
+
dkim: {
|
|
60
|
+
known: number;
|
|
61
|
+
pass: number;
|
|
62
|
+
};
|
|
63
|
+
dmarc: {
|
|
64
|
+
known: number;
|
|
65
|
+
pass: number;
|
|
66
|
+
};
|
|
67
|
+
aligned: {
|
|
68
|
+
known: number;
|
|
69
|
+
pass: number;
|
|
70
|
+
};
|
|
71
|
+
contentScore: {
|
|
72
|
+
sampleSize: number;
|
|
73
|
+
sum: number;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
export type DeliverabilityRate = {
|
|
77
|
+
/** numerator / denominator rounded to 4 places; null when the denominator is 0. */
|
|
78
|
+
value: number | null;
|
|
79
|
+
numerator: number;
|
|
80
|
+
denominator: number;
|
|
81
|
+
/** What the denominator counts, e.g. `settled_probes`. */
|
|
82
|
+
denominatorOf: string;
|
|
83
|
+
lowSample: boolean;
|
|
84
|
+
};
|
|
85
|
+
export declare function deliverabilityRate(numerator: number, denominator: number, denominatorOf: string, minSample?: number): DeliverabilityRate;
|
|
86
|
+
export type PlacementEvidence = {
|
|
87
|
+
probes: number;
|
|
88
|
+
outcomes: Record<PlacementObservation["outcome"], number>;
|
|
89
|
+
folders: Record<PlacementFolder, number>;
|
|
90
|
+
/** Inbox observations by tab; `unknown` when the receiver did not classify the message. */
|
|
91
|
+
tabs: Record<PlacementTab, number>;
|
|
92
|
+
/** Settled probes: inbox, spam, other or not observed. Failed sends and blocked reads are excluded. */
|
|
93
|
+
settled: number;
|
|
94
|
+
inboxRate: DeliverabilityRate;
|
|
95
|
+
spamRate: DeliverabilityRate;
|
|
96
|
+
notObservedRate: DeliverabilityRate;
|
|
97
|
+
/** Gmail tabs: share of inbox probes Gmail classified into a tab. */
|
|
98
|
+
primaryRate: DeliverabilityRate;
|
|
99
|
+
promotionsRate: DeliverabilityRate;
|
|
100
|
+
/** Outlook: Focused share of inbox probes classified Focused or Other. */
|
|
101
|
+
focusedRate: DeliverabilityRate;
|
|
102
|
+
spfPassRate: DeliverabilityRate;
|
|
103
|
+
dkimPassRate: DeliverabilityRate;
|
|
104
|
+
dmarcPassRate: DeliverabilityRate;
|
|
105
|
+
alignedRate: DeliverabilityRate;
|
|
106
|
+
contentScore: {
|
|
107
|
+
average: number | null;
|
|
108
|
+
sampleSize: number;
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
export declare function emptyPlacementCounts(): PlacementCounts;
|
|
112
|
+
export declare function settledPlacementProbes(counts: PlacementCounts): number;
|
|
113
|
+
export declare function summarizePlacementCounts(counts: PlacementCounts): PlacementEvidence;
|
|
114
|
+
/** Real campaign outcomes in the same period; bounces and replies are period co-occurrence, not a send cohort. */
|
|
115
|
+
export type CampaignOutcomes = {
|
|
116
|
+
sends: number;
|
|
117
|
+
bounces: number;
|
|
118
|
+
replies: number;
|
|
119
|
+
bounceRate: DeliverabilityRate;
|
|
120
|
+
replyRate: DeliverabilityRate;
|
|
121
|
+
};
|
|
122
|
+
export declare function campaignOutcomes(sends: number, bounces: number, replies: number): CampaignOutcomes;
|
|
123
|
+
export type DeliverabilityEvidenceQuery = {
|
|
124
|
+
senderDomain?: string;
|
|
125
|
+
senderMailboxId?: string;
|
|
126
|
+
contentHash?: string;
|
|
127
|
+
receiverProvider?: PlacementReceiverProvider;
|
|
128
|
+
since: string;
|
|
129
|
+
until: string;
|
|
130
|
+
bucket: DeliverabilityEvidenceBucketSize;
|
|
131
|
+
};
|
|
132
|
+
export type DeliverabilityCheckState = {
|
|
133
|
+
kind: DeliverabilityCheckKind;
|
|
134
|
+
subject: string;
|
|
135
|
+
/** `never_run` when no run of this kind exists for the subject before the window end. */
|
|
136
|
+
state: DeliverabilityRunStatus | "never_run";
|
|
137
|
+
runId: string | null;
|
|
138
|
+
startedAt: string | null;
|
|
139
|
+
findingCounts: Record<DeliverabilityFindingStatus, number>;
|
|
140
|
+
failing: string[];
|
|
141
|
+
warning: string[];
|
|
142
|
+
};
|
|
143
|
+
export type DeliverabilityFindingChange = {
|
|
144
|
+
kind: DeliverabilityCheckKind;
|
|
145
|
+
subject: string;
|
|
146
|
+
check: string;
|
|
147
|
+
from: DeliverabilityFindingStatus;
|
|
148
|
+
to: DeliverabilityFindingStatus;
|
|
149
|
+
at: string;
|
|
150
|
+
runId: string;
|
|
151
|
+
};
|
|
152
|
+
export type PlacementChangeSignalRef = {
|
|
153
|
+
id: string;
|
|
154
|
+
occurredAt: string;
|
|
155
|
+
transition: PlacementTransitionKind;
|
|
156
|
+
senderDomain: string;
|
|
157
|
+
receiverProvider: PlacementReceiverProvider;
|
|
158
|
+
summary: string | null;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* A capped list that says it is capped: `total` counts every match in the window, `items` holds
|
|
162
|
+
* at most the cap in `order`, and `truncated` is true when `total` exceeds `items.length`.
|
|
163
|
+
*/
|
|
164
|
+
export type DeliverabilityEvidenceList<T> = {
|
|
165
|
+
items: T[];
|
|
166
|
+
total: number;
|
|
167
|
+
truncated: boolean;
|
|
168
|
+
order: string;
|
|
169
|
+
};
|
|
170
|
+
export declare function deliverabilityEvidenceList<T>(items: T[], total: number, order: string): DeliverabilityEvidenceList<T>;
|
|
171
|
+
export type DeliverabilityEvidenceBucket = {
|
|
172
|
+
start: string;
|
|
173
|
+
end: string;
|
|
174
|
+
placement: PlacementEvidence;
|
|
175
|
+
/** Null when campaign outcomes cannot be narrowed to this subject (see `campaignAvailability`). */
|
|
176
|
+
campaign: CampaignOutcomes | null;
|
|
177
|
+
};
|
|
178
|
+
export type DeliverabilityEvidence = {
|
|
179
|
+
subject: {
|
|
180
|
+
senderDomain: string | null;
|
|
181
|
+
senderMailboxId: string | null;
|
|
182
|
+
contentHash: string | null;
|
|
183
|
+
receiverProvider: PlacementReceiverProvider | null;
|
|
184
|
+
/** The domain whose checks are reported: the filter, or the sender mailbox's domain. */
|
|
185
|
+
checkedDomain: string | null;
|
|
186
|
+
};
|
|
187
|
+
window: {
|
|
188
|
+
since: string;
|
|
189
|
+
until: string;
|
|
190
|
+
bucket: DeliverabilityEvidenceBucketSize;
|
|
191
|
+
timezone: "UTC";
|
|
192
|
+
bucketCount: number;
|
|
193
|
+
};
|
|
194
|
+
basis: {
|
|
195
|
+
placementTime: string;
|
|
196
|
+
campaign: string;
|
|
197
|
+
};
|
|
198
|
+
totals: {
|
|
199
|
+
placement: PlacementEvidence;
|
|
200
|
+
campaign: CampaignOutcomes | null;
|
|
201
|
+
};
|
|
202
|
+
buckets: DeliverabilityEvidenceBucket[];
|
|
203
|
+
byReceiverProvider: Array<{
|
|
204
|
+
receiverProvider: PlacementReceiverProvider;
|
|
205
|
+
placement: PlacementEvidence;
|
|
206
|
+
}>;
|
|
207
|
+
bySource: Array<{
|
|
208
|
+
source: PlacementObservationSource;
|
|
209
|
+
probes: number;
|
|
210
|
+
}>;
|
|
211
|
+
campaignAvailability: {
|
|
212
|
+
available: boolean;
|
|
213
|
+
reason: string | null;
|
|
214
|
+
};
|
|
215
|
+
checks: DeliverabilityCheckState[];
|
|
216
|
+
/** Copy hashes seen in the window, most probes first. */
|
|
217
|
+
contentScores: DeliverabilityEvidenceList<{
|
|
218
|
+
contentHash: string;
|
|
219
|
+
probes: number;
|
|
220
|
+
latest: DeliverabilityCheckState;
|
|
221
|
+
}>;
|
|
222
|
+
/** Finding status changes of the checked domain, newest first. */
|
|
223
|
+
changes: DeliverabilityEvidenceList<DeliverabilityFindingChange>;
|
|
224
|
+
/** Placement Signals emitted in the window, newest first. */
|
|
225
|
+
signals: DeliverabilityEvidenceList<PlacementChangeSignalRef>;
|
|
226
|
+
};
|
|
227
|
+
/** How many UTC-aligned buckets a window spans, computed without building them, so a bound can be checked first. */
|
|
228
|
+
export declare function deliverabilityEvidenceBucketCount(since: Date, until: Date, bucket: DeliverabilityEvidenceBucketSize): number;
|
|
229
|
+
export declare function deliverabilityEvidenceBuckets(since: Date, until: Date, bucket: DeliverabilityEvidenceBucketSize): {
|
|
230
|
+
origin: Date;
|
|
231
|
+
stepMs: number;
|
|
232
|
+
buckets: Array<{
|
|
233
|
+
start: Date;
|
|
234
|
+
end: Date;
|
|
235
|
+
}>;
|
|
236
|
+
};
|
|
237
|
+
export type PlacementRunSide = {
|
|
238
|
+
ref: string;
|
|
239
|
+
sources: PlacementObservationSource[];
|
|
240
|
+
at: string;
|
|
241
|
+
senderDomains: string[];
|
|
242
|
+
contentHashes: string[];
|
|
243
|
+
variantKeys: string[];
|
|
244
|
+
placement: PlacementEvidence;
|
|
245
|
+
};
|
|
246
|
+
export type DeliverabilityCheckSide = {
|
|
247
|
+
runId: string;
|
|
248
|
+
kind: DeliverabilityCheckKind;
|
|
249
|
+
subject: string;
|
|
250
|
+
status: DeliverabilityRunStatus;
|
|
251
|
+
startedAt: string;
|
|
252
|
+
engineVersion: string | null;
|
|
253
|
+
findingCounts: Record<DeliverabilityFindingStatus, number>;
|
|
254
|
+
};
|
|
255
|
+
export type DeliverabilityFindingDiff = {
|
|
256
|
+
check: string;
|
|
257
|
+
a: DeliverabilityFindingStatus | null;
|
|
258
|
+
b: DeliverabilityFindingStatus | null;
|
|
259
|
+
};
|
|
260
|
+
export type DeliverabilityRuleSymbolDiff = {
|
|
261
|
+
added: string[];
|
|
262
|
+
removed: string[];
|
|
263
|
+
};
|
|
264
|
+
export type DeliverabilityComparison = {
|
|
265
|
+
kind: "placement";
|
|
266
|
+
a: PlacementRunSide;
|
|
267
|
+
b: PlacementRunSide;
|
|
268
|
+
byReceiverProvider: Array<{
|
|
269
|
+
receiverProvider: PlacementReceiverProvider;
|
|
270
|
+
a: PlacementEvidence;
|
|
271
|
+
b: PlacementEvidence;
|
|
272
|
+
inboxRateDelta: number | null;
|
|
273
|
+
}>;
|
|
274
|
+
inboxRateDelta: number | null;
|
|
275
|
+
content: {
|
|
276
|
+
changed: boolean;
|
|
277
|
+
/** Copy hashes of either run, sorted; rule symbols are diffed over the listed hashes only. */
|
|
278
|
+
scores: DeliverabilityEvidenceList<{
|
|
279
|
+
contentHash: string;
|
|
280
|
+
side: "a" | "b" | "both";
|
|
281
|
+
latest: DeliverabilityCheckState;
|
|
282
|
+
}>;
|
|
283
|
+
ruleSymbols: DeliverabilityRuleSymbolDiff;
|
|
284
|
+
};
|
|
285
|
+
/** Sender domains of either run, sorted; domain checks are compared for the listed domains only. */
|
|
286
|
+
domains: DeliverabilityEvidenceList<string>;
|
|
287
|
+
/** Domain check states as of each run's time; only checks whose status differs are listed. */
|
|
288
|
+
findingChanges: Array<{
|
|
289
|
+
kind: DeliverabilityCheckKind;
|
|
290
|
+
subject: string;
|
|
291
|
+
aRunId: string | null;
|
|
292
|
+
bRunId: string | null;
|
|
293
|
+
changes: DeliverabilityFindingDiff[];
|
|
294
|
+
}>;
|
|
295
|
+
} | {
|
|
296
|
+
kind: "check";
|
|
297
|
+
checkKind: DeliverabilityCheckKind;
|
|
298
|
+
a: DeliverabilityCheckSide;
|
|
299
|
+
b: DeliverabilityCheckSide;
|
|
300
|
+
sameSubject: boolean;
|
|
301
|
+
changes: DeliverabilityFindingDiff[];
|
|
302
|
+
unchanged: number;
|
|
303
|
+
ruleSymbols: DeliverabilityRuleSymbolDiff | null;
|
|
304
|
+
};
|
|
305
|
+
/** Status by check code on each side → only the codes whose status differs. */
|
|
306
|
+
export declare function diffFindingStatuses(a: ReadonlyMap<string, DeliverabilityFindingStatus>, b: ReadonlyMap<string, DeliverabilityFindingStatus>): {
|
|
307
|
+
changes: DeliverabilityFindingDiff[];
|
|
308
|
+
unchanged: number;
|
|
309
|
+
};
|
|
310
|
+
export declare function diffRuleSymbols(a: Iterable<string>, b: Iterable<string>): DeliverabilityRuleSymbolDiff;
|
|
311
|
+
export type PlacementTransition = {
|
|
312
|
+
transition: PlacementTransitionKind;
|
|
313
|
+
senderDomain: string;
|
|
314
|
+
receiverProvider: PlacementReceiverProvider;
|
|
315
|
+
baseline: DeliverabilityRate;
|
|
316
|
+
current: DeliverabilityRate;
|
|
317
|
+
/** current − baseline of the measured rate (negative for a drop). */
|
|
318
|
+
delta: number;
|
|
319
|
+
threshold: number;
|
|
320
|
+
/** The minimum denominator each window needed; it counts what `baseline.denominatorOf` names. */
|
|
321
|
+
minSample: number;
|
|
322
|
+
/** The second rate a tab shift also requires (Promotions share). */
|
|
323
|
+
secondary?: {
|
|
324
|
+
baseline: DeliverabilityRate;
|
|
325
|
+
current: DeliverabilityRate;
|
|
326
|
+
delta: number;
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
/** Pure transition rules; the thresholds above are the whole policy. */
|
|
330
|
+
export declare function detectPlacementTransitions(input: {
|
|
331
|
+
senderDomain: string;
|
|
332
|
+
receiverProvider: PlacementReceiverProvider;
|
|
333
|
+
baseline: PlacementCounts;
|
|
334
|
+
current: PlacementCounts;
|
|
335
|
+
}): PlacementTransition[];
|
|
336
|
+
export declare function placementTransitionSummary(t: PlacementTransition): string;
|
|
337
|
+
/** The Signal's typed payload (`ox_model.activities.metadata`), versioned by `schema`. */
|
|
338
|
+
export type PlacementChangeSignalMetadata = {
|
|
339
|
+
schema: typeof PLACEMENT_SIGNAL_SCHEMA;
|
|
340
|
+
transition: PlacementTransitionKind;
|
|
341
|
+
sender_domain: string;
|
|
342
|
+
receiver_provider: PlacementReceiverProvider;
|
|
343
|
+
window: {
|
|
344
|
+
baseline_since: string;
|
|
345
|
+
current_since: string;
|
|
346
|
+
until: string;
|
|
347
|
+
};
|
|
348
|
+
baseline: DeliverabilityRate;
|
|
349
|
+
current: DeliverabilityRate;
|
|
350
|
+
delta: number;
|
|
351
|
+
threshold: number;
|
|
352
|
+
/** Minimum denominator per window for this transition; it counts `baseline.denominatorOf`. */
|
|
353
|
+
min_sample: number;
|
|
354
|
+
secondary: PlacementTransition["secondary"] | null;
|
|
355
|
+
/**
|
|
356
|
+
* Sample sizes behind the transition. `*_settled` always counts settled probes; the Gmail-tab
|
|
357
|
+
* counts are the Primary/Promotions denominators, set only for `primary_to_promotions`.
|
|
358
|
+
*/
|
|
359
|
+
evidence: {
|
|
360
|
+
baseline_settled: number;
|
|
361
|
+
current_settled: number;
|
|
362
|
+
baseline_gmail_tabbed: number | null;
|
|
363
|
+
current_gmail_tabbed: number | null;
|
|
364
|
+
latest_observation_at: string | null;
|
|
365
|
+
};
|
|
366
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deliverability learning loop's read contract: evidence over stored check runs, placement
|
|
3
|
+
* observations and real campaign outcomes. Every rate carries its numerator, denominator and what
|
|
4
|
+
* the denominator counts; an empty bucket is reported as zeros, never interpolated.
|
|
5
|
+
*/
|
|
6
|
+
export const DELIVERABILITY_EVIDENCE_BUCKETS = ["day", "week"];
|
|
7
|
+
/** Window bound per bucket size; a week window covers the whole 400-day retention. */
|
|
8
|
+
export const DELIVERABILITY_EVIDENCE_MAX_BUCKETS = { day: 120, week: 60 };
|
|
9
|
+
/** Below this denominator a rate is still reported, but flagged as a low sample. */
|
|
10
|
+
export const DELIVERABILITY_EVIDENCE_MIN_SAMPLE = 8;
|
|
11
|
+
export const DELIVERABILITY_EVIDENCE_MAX_CONTENT_HASHES = 10;
|
|
12
|
+
export const DELIVERABILITY_EVIDENCE_MAX_CHANGES = 100;
|
|
13
|
+
export const DELIVERABILITY_EVIDENCE_MAX_SIGNALS = 50;
|
|
14
|
+
/** Domain-subject checks whose latest state and status changes the evidence reports. */
|
|
15
|
+
export const DELIVERABILITY_EVIDENCE_DOMAIN_CHECKS = ["dns_auth", "bimi", "blacklist"];
|
|
16
|
+
/** Content-score findings name each triggered rule as `content.rule.<symbol>`. */
|
|
17
|
+
export const DELIVERABILITY_CONTENT_RULE_PREFIX = "content.rule.";
|
|
18
|
+
/** Placement time: when the probe was sent, else when it was read, else when it was stored. */
|
|
19
|
+
export const PLACEMENT_EVIDENCE_TIME = "sent_at, else observed_at, else recorded_at";
|
|
20
|
+
/**
|
|
21
|
+
* Placement-change Signals: a sender domain × receiver provider pair is compared over a rolling
|
|
22
|
+
* current window against the window before it. Placement is directional evidence only, so a
|
|
23
|
+
* transition emits a Signal and never pauses a sender.
|
|
24
|
+
*/
|
|
25
|
+
export const PLACEMENT_SIGNAL_CURRENT_WINDOW_DAYS = 7;
|
|
26
|
+
export const PLACEMENT_SIGNAL_BASELINE_WINDOW_DAYS = 28;
|
|
27
|
+
/** Settled probes (inbox, spam, other or not observed) each window needs before a rate is trusted. */
|
|
28
|
+
export const PLACEMENT_SIGNAL_MIN_SAMPLE = 8;
|
|
29
|
+
/** Absolute inbox-rate drop (0.25 = 25 percentage points) that counts as a transition. */
|
|
30
|
+
export const PLACEMENT_SIGNAL_INBOX_DROP = 0.25;
|
|
31
|
+
/** Gmail-tab-classified inbox probes each window needs before a tab shift is trusted. */
|
|
32
|
+
export const PLACEMENT_SIGNAL_MIN_TAB_SAMPLE = 6;
|
|
33
|
+
/** Primary share must fall, and Promotions share rise, by at least this much. */
|
|
34
|
+
export const PLACEMENT_SIGNAL_TAB_SHIFT = 0.25;
|
|
35
|
+
/** One Signal per domain × provider × transition within this many days. */
|
|
36
|
+
export const PLACEMENT_SIGNAL_COOLDOWN_DAYS = 7;
|
|
37
|
+
export const PLACEMENT_SIGNAL_MAX_PAIRS = 500;
|
|
38
|
+
export const PLACEMENT_SIGNAL_MAX_EMITS = 50;
|
|
39
|
+
export const PLACEMENT_SIGNAL_TYPE = "placement_changed";
|
|
40
|
+
export const PLACEMENT_SIGNAL_SOURCE_PROVIDER = "oxygen_deliverability";
|
|
41
|
+
export const PLACEMENT_SIGNAL_SCHEMA = "placement_changed.v1";
|
|
42
|
+
export const PLACEMENT_TRANSITIONS = ["inbox_rate_drop", "primary_to_promotions"];
|
|
43
|
+
const OUTCOMES = ["observed", "not_observed", "send_failed", "send_unknown", "read_blocked"];
|
|
44
|
+
const FOLDERS = ["inbox", "spam", "other", "not_observed", "unknown"];
|
|
45
|
+
const TABS = ["primary", "promotions", "social", "updates", "forums", "focused", "other", "unknown"];
|
|
46
|
+
const GMAIL_TABS = ["primary", "promotions", "social", "updates", "forums"];
|
|
47
|
+
export const PLACEMENT_COUNT_OUTCOMES = OUTCOMES;
|
|
48
|
+
export const PLACEMENT_COUNT_FOLDERS = FOLDERS;
|
|
49
|
+
export const PLACEMENT_COUNT_TABS = TABS;
|
|
50
|
+
export function deliverabilityRate(numerator, denominator, denominatorOf, minSample = DELIVERABILITY_EVIDENCE_MIN_SAMPLE) {
|
|
51
|
+
return {
|
|
52
|
+
value: denominator > 0 ? Math.round((numerator / denominator) * 10_000) / 10_000 : null,
|
|
53
|
+
numerator, denominator, denominatorOf, lowSample: denominator < minSample,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const zeros = (keys) => Object.fromEntries(keys.map((key) => [key, 0]));
|
|
57
|
+
export function emptyPlacementCounts() {
|
|
58
|
+
return {
|
|
59
|
+
probes: 0, outcomes: zeros(OUTCOMES), folders: zeros(FOLDERS), tabs: zeros(TABS),
|
|
60
|
+
spf: { known: 0, pass: 0 }, dkim: { known: 0, pass: 0 }, dmarc: { known: 0, pass: 0 }, aligned: { known: 0, pass: 0 },
|
|
61
|
+
contentScore: { sampleSize: 0, sum: 0 },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function settledPlacementProbes(counts) {
|
|
65
|
+
return counts.folders.inbox + counts.folders.spam + counts.folders.other + counts.folders.not_observed;
|
|
66
|
+
}
|
|
67
|
+
const gmailTabbed = (counts) => GMAIL_TABS.reduce((sum, tab) => sum + counts.tabs[tab], 0);
|
|
68
|
+
export function summarizePlacementCounts(counts) {
|
|
69
|
+
const settled = settledPlacementProbes(counts);
|
|
70
|
+
const gmail = gmailTabbed(counts);
|
|
71
|
+
const outlook = counts.tabs.focused + counts.tabs.other;
|
|
72
|
+
return {
|
|
73
|
+
probes: counts.probes, outcomes: { ...counts.outcomes }, folders: { ...counts.folders }, tabs: { ...counts.tabs }, settled,
|
|
74
|
+
inboxRate: deliverabilityRate(counts.folders.inbox, settled, "settled_probes"),
|
|
75
|
+
spamRate: deliverabilityRate(counts.folders.spam, settled, "settled_probes"),
|
|
76
|
+
notObservedRate: deliverabilityRate(counts.folders.not_observed, settled, "settled_probes"),
|
|
77
|
+
primaryRate: deliverabilityRate(counts.tabs.primary, gmail, "inbox_probes_with_gmail_tab", PLACEMENT_SIGNAL_MIN_TAB_SAMPLE),
|
|
78
|
+
promotionsRate: deliverabilityRate(counts.tabs.promotions, gmail, "inbox_probes_with_gmail_tab", PLACEMENT_SIGNAL_MIN_TAB_SAMPLE),
|
|
79
|
+
focusedRate: deliverabilityRate(counts.tabs.focused, outlook, "inbox_probes_with_outlook_focus", PLACEMENT_SIGNAL_MIN_TAB_SAMPLE),
|
|
80
|
+
spfPassRate: deliverabilityRate(counts.spf.pass, counts.spf.known, "probes_with_spf_result"),
|
|
81
|
+
dkimPassRate: deliverabilityRate(counts.dkim.pass, counts.dkim.known, "probes_with_dkim_result"),
|
|
82
|
+
dmarcPassRate: deliverabilityRate(counts.dmarc.pass, counts.dmarc.known, "probes_with_dmarc_result"),
|
|
83
|
+
alignedRate: deliverabilityRate(counts.aligned.pass, counts.aligned.known, "probes_with_alignment_result"),
|
|
84
|
+
contentScore: {
|
|
85
|
+
average: counts.contentScore.sampleSize > 0 ? Math.round((counts.contentScore.sum / counts.contentScore.sampleSize) * 100) / 100 : null,
|
|
86
|
+
sampleSize: counts.contentScore.sampleSize,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function campaignOutcomes(sends, bounces, replies) {
|
|
91
|
+
return { sends, bounces, replies, bounceRate: deliverabilityRate(bounces, sends, "campaign_sends"), replyRate: deliverabilityRate(replies, sends, "campaign_sends") };
|
|
92
|
+
}
|
|
93
|
+
export function deliverabilityEvidenceList(items, total, order) {
|
|
94
|
+
const count = Math.max(total, items.length);
|
|
95
|
+
return { items, total: count, truncated: count > items.length, order };
|
|
96
|
+
}
|
|
97
|
+
/** UTC calendar buckets covering [since, until); the outer buckets are clipped to the window. */
|
|
98
|
+
function evidenceBucketGrid(since, bucket) {
|
|
99
|
+
const day = 86_400_000;
|
|
100
|
+
const stepMs = bucket === "day" ? day : 7 * day;
|
|
101
|
+
const floorDay = Math.floor(since.getTime() / day) * day;
|
|
102
|
+
// 1970-01-01 was a Thursday; ISO weeks start on Monday.
|
|
103
|
+
return { origin: bucket === "day" ? floorDay : floorDay - (((new Date(floorDay).getUTCDay() + 6) % 7) * day), stepMs };
|
|
104
|
+
}
|
|
105
|
+
/** How many UTC-aligned buckets a window spans, computed without building them, so a bound can be checked first. */
|
|
106
|
+
export function deliverabilityEvidenceBucketCount(since, until, bucket) {
|
|
107
|
+
const { origin, stepMs } = evidenceBucketGrid(since, bucket);
|
|
108
|
+
return Math.max(0, Math.ceil((until.getTime() - origin) / stepMs));
|
|
109
|
+
}
|
|
110
|
+
export function deliverabilityEvidenceBuckets(since, until, bucket) {
|
|
111
|
+
const { origin, stepMs } = evidenceBucketGrid(since, bucket);
|
|
112
|
+
const buckets = [];
|
|
113
|
+
for (let start = origin; start < until.getTime(); start += stepMs) {
|
|
114
|
+
buckets.push({ start: new Date(Math.max(start, since.getTime())), end: new Date(Math.min(start + stepMs, until.getTime())) });
|
|
115
|
+
}
|
|
116
|
+
return { origin: new Date(origin), stepMs, buckets };
|
|
117
|
+
}
|
|
118
|
+
/** Status by check code on each side → only the codes whose status differs. */
|
|
119
|
+
export function diffFindingStatuses(a, b) {
|
|
120
|
+
const codes = [...new Set([...a.keys(), ...b.keys()])].sort();
|
|
121
|
+
const changes = [];
|
|
122
|
+
let unchanged = 0;
|
|
123
|
+
for (const check of codes) {
|
|
124
|
+
const left = a.get(check) ?? null;
|
|
125
|
+
const right = b.get(check) ?? null;
|
|
126
|
+
if (left === right)
|
|
127
|
+
unchanged += 1;
|
|
128
|
+
else
|
|
129
|
+
changes.push({ check, a: left, b: right });
|
|
130
|
+
}
|
|
131
|
+
return { changes, unchanged };
|
|
132
|
+
}
|
|
133
|
+
export function diffRuleSymbols(a, b) {
|
|
134
|
+
const symbols = (codes) => new Set([...codes].filter((code) => code.startsWith(DELIVERABILITY_CONTENT_RULE_PREFIX)).map((code) => code.slice(DELIVERABILITY_CONTENT_RULE_PREFIX.length)));
|
|
135
|
+
const left = symbols(a);
|
|
136
|
+
const right = symbols(b);
|
|
137
|
+
return { added: [...right].filter((s) => !left.has(s)).sort(), removed: [...left].filter((s) => !right.has(s)).sort() };
|
|
138
|
+
}
|
|
139
|
+
const round4 = (value) => Math.round(value * 10_000) / 10_000;
|
|
140
|
+
// Rates are rounded to 4 places, so a threshold met exactly must not miss on float noise.
|
|
141
|
+
const meets = (delta, threshold) => delta >= threshold - 1e-9;
|
|
142
|
+
/** Pure transition rules; the thresholds above are the whole policy. */
|
|
143
|
+
export function detectPlacementTransitions(input) {
|
|
144
|
+
const base = summarizePlacementCounts(input.baseline);
|
|
145
|
+
const now = summarizePlacementCounts(input.current);
|
|
146
|
+
const found = [];
|
|
147
|
+
const pair = { senderDomain: input.senderDomain, receiverProvider: input.receiverProvider };
|
|
148
|
+
if (base.settled >= PLACEMENT_SIGNAL_MIN_SAMPLE && now.settled >= PLACEMENT_SIGNAL_MIN_SAMPLE
|
|
149
|
+
&& meets(base.inboxRate.value - now.inboxRate.value, PLACEMENT_SIGNAL_INBOX_DROP)) {
|
|
150
|
+
found.push({ transition: "inbox_rate_drop", ...pair, baseline: base.inboxRate, current: now.inboxRate,
|
|
151
|
+
delta: round4(now.inboxRate.value - base.inboxRate.value), threshold: PLACEMENT_SIGNAL_INBOX_DROP, minSample: PLACEMENT_SIGNAL_MIN_SAMPLE });
|
|
152
|
+
}
|
|
153
|
+
if (base.primaryRate.denominator >= PLACEMENT_SIGNAL_MIN_TAB_SAMPLE && now.primaryRate.denominator >= PLACEMENT_SIGNAL_MIN_TAB_SAMPLE
|
|
154
|
+
&& meets(base.primaryRate.value - now.primaryRate.value, PLACEMENT_SIGNAL_TAB_SHIFT)
|
|
155
|
+
&& meets(now.promotionsRate.value - base.promotionsRate.value, PLACEMENT_SIGNAL_TAB_SHIFT)) {
|
|
156
|
+
found.push({ transition: "primary_to_promotions", ...pair, baseline: base.primaryRate, current: now.primaryRate,
|
|
157
|
+
delta: round4(now.primaryRate.value - base.primaryRate.value), threshold: PLACEMENT_SIGNAL_TAB_SHIFT,
|
|
158
|
+
minSample: PLACEMENT_SIGNAL_MIN_TAB_SAMPLE, secondary: { baseline: base.promotionsRate, current: now.promotionsRate, delta: round4(now.promotionsRate.value - base.promotionsRate.value) } });
|
|
159
|
+
}
|
|
160
|
+
return found;
|
|
161
|
+
}
|
|
162
|
+
const PROVIDER_LABEL = { google: "Google", microsoft: "Microsoft", yahoo: "Yahoo", other: "other receivers", unknown: "unknown receivers" };
|
|
163
|
+
const percent = (rate) => (rate.value === null ? "n/a" : `${Math.round(rate.value * 100)}%`);
|
|
164
|
+
export function placementTransitionSummary(t) {
|
|
165
|
+
const where = `${t.senderDomain} at ${PROVIDER_LABEL[t.receiverProvider]}`;
|
|
166
|
+
return t.transition === "inbox_rate_drop"
|
|
167
|
+
? `Inbox rate for ${where} fell from ${percent(t.baseline)} (n=${t.baseline.denominator}) to ${percent(t.current)} (n=${t.current.denominator}).`
|
|
168
|
+
: `Gmail Primary share for ${where} fell from ${percent(t.baseline)} to ${percent(t.current)} while Promotions rose from ${percent(t.secondary.baseline)} to ${percent(t.secondary.current)} (n=${t.baseline.denominator} → n=${t.current.denominator}).`;
|
|
169
|
+
}
|