@sellable/mcp 0.1.793 → 0.1.794
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-tool-names.d.ts +2 -2
- package/dist/agent-tool-names.js +14 -0
- package/dist/scheduler-envelope.d.ts +190 -0
- package/dist/scheduler-envelope.js +412 -0
- package/dist/server.js +28 -2
- package/dist/tools/campaign-message-preparation.d.ts +25 -0
- package/dist/tools/campaign-message-preparation.js +37 -0
- package/dist/tools/campaign-processing.d.ts +74 -1
- package/dist/tools/campaign-processing.js +57 -1
- package/dist/tools/prompts.d.ts +15 -3
- package/dist/tools/prompts.js +25 -0
- package/dist/tools/refill-executors.d.ts +43 -70
- package/dist/tools/refill-executors.js +201 -273
- package/dist/tools/refill-sends.d.ts +3 -3
- package/dist/tools/refill-sends.js +1 -1
- package/dist/tools/refill-target-plan.js +1 -1
- package/dist/tools/refill-v3-advance-contract.d.ts +618 -0
- package/dist/tools/refill-v3-advance-contract.js +920 -0
- package/dist/tools/refill-v3-advance.d.ts +397 -0
- package/dist/tools/refill-v3-advance.js +75 -0
- package/dist/tools/refill-v3-attention-packet-contract.d.ts +608 -0
- package/dist/tools/refill-v3-attention-packet-contract.js +911 -0
- package/dist/tools/refill-v3-campaign-work.d.ts +324 -0
- package/dist/tools/refill-v3-campaign-work.js +1015 -0
- package/dist/tools/refill-v3-continue-contract.d.ts +433 -0
- package/dist/tools/refill-v3-continue-contract.js +112 -0
- package/dist/tools/refill-v3-continue.d.ts +501 -0
- package/dist/tools/refill-v3-continue.js +654 -0
- package/dist/tools/refill-v3-edit-selected-campaign-contract.d.ts +208 -0
- package/dist/tools/refill-v3-edit-selected-campaign-contract.js +351 -0
- package/dist/tools/refill-v3-edit-selected-campaign.d.ts +232 -0
- package/dist/tools/refill-v3-edit-selected-campaign.js +147 -0
- package/dist/tools/refill-v3-fill-ready-contract.d.ts +198 -0
- package/dist/tools/refill-v3-fill-ready-contract.js +324 -0
- package/dist/tools/refill-v3-fill-ready.d.ts +199 -0
- package/dist/tools/refill-v3-fill-ready.js +136 -0
- package/dist/tools/refill-v3-source-family-dispatch.d.ts +249 -0
- package/dist/tools/refill-v3-source-family-dispatch.js +514 -0
- package/dist/tools/refill-v3-waterfall-contract.d.ts +156 -0
- package/dist/tools/refill-v3-waterfall-contract.js +181 -0
- package/dist/tools/refill-v3-waterfall.d.ts +136 -0
- package/dist/tools/refill-v3-waterfall.js +161 -0
- package/dist/tools/refill-v3-world-state-contract.d.ts +666 -0
- package/dist/tools/refill-v3-world-state-contract.js +790 -0
- package/dist/tools/refill-v3-world-state.d.ts +592 -0
- package/dist/tools/refill-v3-world-state.js +137 -0
- package/dist/tools/registry.d.ts +1710 -3
- package/dist/tools/registry.js +19 -0
- package/package.json +1 -1
- package/skills/refill-sends/SKILL.md +89 -4
- package/skills/refill-sends-waterfall-order/SKILL.md +174 -0
- package/skills/refill-sends-work-campaign/SKILL.md +253 -0
- package/skills/refill-sends-workflow/SKILL.md +112 -5
- package/skills/refill-sends-workflow/core/flow.v3.json +209 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refill V3 world-state PACKAGE seam.
|
|
3
|
+
*
|
|
4
|
+
* This module is the SOLE owner of the public result contract:
|
|
5
|
+
* - the canonical result/row/campaign/reason/effect types;
|
|
6
|
+
* - the finite output vocabularies and the forbidden-leak key list;
|
|
7
|
+
* - `REFILL_V3_WORLD_STATE_OUTPUT_SCHEMA`; and
|
|
8
|
+
* - `decodeRefillWorldStateResult`, the ONE unknown-value decoder.
|
|
9
|
+
*
|
|
10
|
+
* It lives inside `@sellable/mcp` so the public MCP tool can compile and ship
|
|
11
|
+
* the contract. The app seam `src/lib/refill-v3/world-state-contract.ts`
|
|
12
|
+
* imports FROM here and type-only re-exports these symbols; nothing here may
|
|
13
|
+
* import app `src/**`, and no consumer may copy the decoder.
|
|
14
|
+
*
|
|
15
|
+
* Decoding rules:
|
|
16
|
+
* - hand-rolled type guards plus as-const JSON-Schema literals (A02-NO-ZOD);
|
|
17
|
+
* - strict inbound, tolerant outbound, and lossless;
|
|
18
|
+
* - an unrecognized ENUM VALUE drops only the SMALLEST unit that carries it,
|
|
19
|
+
* so one new job kind can never blank a healthy workspace; and
|
|
20
|
+
* - only a malformed envelope, a missing required top-level key, or a policy
|
|
21
|
+
* leak key rejects the whole result.
|
|
22
|
+
*/
|
|
23
|
+
import { REFILL_V3_ACTIVE_JOB_KINDS, REFILL_V3_ACTIVE_JOB_STATUSES, REFILL_V3_LEAD_SOURCE_FAMILIES, REFILL_V3_SCHEMA_VERSION_UNSUPPORTED, REFILL_V3_SUPPLY_BUCKETS, resolveRefillV3SchemaVersion, } from "./refill-v3-attention-packet-contract.js";
|
|
24
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
25
|
+
// Finite output vocabularies
|
|
26
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
27
|
+
export const REFILL_V3_WORLD_STATE_SCHEMA_NAMESPACE = "refill_v3_world_state";
|
|
28
|
+
export const REFILL_V3_WORLD_STATE_SCHEMA_MAJOR = 1;
|
|
29
|
+
export const REFILL_V3_WORLD_STATE_SCHEMA_VERSION = "refill_v3_world_state.v1.0";
|
|
30
|
+
/** The only two Refill V3 lanes in this version. There is no lane input. */
|
|
31
|
+
export const REFILL_V3_WORLD_STATE_LANES = [
|
|
32
|
+
"connection_invite",
|
|
33
|
+
"paid_inmail",
|
|
34
|
+
];
|
|
35
|
+
export const REFILL_V3_WORLD_STATE_REASON_KINDS = [
|
|
36
|
+
"unavailable",
|
|
37
|
+
"retry_required",
|
|
38
|
+
"invalid_campaign_configuration",
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* Exactly three PUBLIC cooldown codes. Canonical `endpoint_cooldown` survives
|
|
42
|
+
* as the separate finite blocker `endpoint_temporarily_unavailable` with a
|
|
43
|
+
* `resumeAt`; it is deliberately not a fourth cooldown.
|
|
44
|
+
*/
|
|
45
|
+
export const REFILL_V3_WORLD_STATE_COOLDOWN_CODES = [
|
|
46
|
+
"global_cooldown",
|
|
47
|
+
"connection_cooldown",
|
|
48
|
+
"paid_inmail_cooldown",
|
|
49
|
+
];
|
|
50
|
+
export const REFILL_V3_WORLD_STATE_REASON_CODES = [
|
|
51
|
+
...REFILL_V3_WORLD_STATE_COOLDOWN_CODES,
|
|
52
|
+
"endpoint_temporarily_unavailable",
|
|
53
|
+
"date_in_past",
|
|
54
|
+
"no_sending_hours",
|
|
55
|
+
"sender_disconnected",
|
|
56
|
+
"sender_limit_reached",
|
|
57
|
+
"daily_target_met",
|
|
58
|
+
"no_eligible_campaign",
|
|
59
|
+
"no_ready_supply",
|
|
60
|
+
"sales_nav_disconnected",
|
|
61
|
+
"insufficient_paid_credits",
|
|
62
|
+
"paid_inmail_connection_health_recheck_required",
|
|
63
|
+
"paid_inmail_credit_refresh_failed",
|
|
64
|
+
"invalid_paid_configuration",
|
|
65
|
+
"campaign_table_missing",
|
|
66
|
+
"campaign_sequence_invalid",
|
|
67
|
+
/**
|
|
68
|
+
* A brand-new campaign whose lead source is not chosen yet, or whose source
|
|
69
|
+
* metadata names more than one family. The closed family vocabulary
|
|
70
|
+
* deliberately gains no `unknown` member, so the campaign is emitted with
|
|
71
|
+
* accurate facts and this typed reason instead of an invented family that
|
|
72
|
+
* would make the decoder reject the ENTIRE workspace observation.
|
|
73
|
+
*/
|
|
74
|
+
"source_family_unresolved",
|
|
75
|
+
/**
|
|
76
|
+
* A live queued/running job already owns this campaign's next unit of work.
|
|
77
|
+
* It is the duplicate-work signal and outranks stale actionable supply.
|
|
78
|
+
*/
|
|
79
|
+
"campaign_work_in_flight",
|
|
80
|
+
];
|
|
81
|
+
export const REFILL_V3_WORLD_STATE_CAMPAIGN_STATUSES = [
|
|
82
|
+
"ACTIVE",
|
|
83
|
+
"PAUSED",
|
|
84
|
+
];
|
|
85
|
+
export const REFILL_V3_WORLD_STATE_EFFECTIVENESS = [
|
|
86
|
+
"unknown",
|
|
87
|
+
"low",
|
|
88
|
+
"medium",
|
|
89
|
+
"high",
|
|
90
|
+
];
|
|
91
|
+
export const REFILL_V3_WORLD_STATE_SUPPLY_HEALTH = [
|
|
92
|
+
"unknown",
|
|
93
|
+
"healthy",
|
|
94
|
+
"thin",
|
|
95
|
+
"dry",
|
|
96
|
+
];
|
|
97
|
+
/**
|
|
98
|
+
* The PUBLIC paid-credit cache outcome. Observation performs zero product
|
|
99
|
+
* mutations, and this is the one bookkeeping effect it is allowed to report,
|
|
100
|
+
* which is why the tool must never claim literal `readOnlyHint: true`.
|
|
101
|
+
*
|
|
102
|
+
* The app's internal `REFILL_V3_PAID_CREDIT_CACHE_EFFECTS` is the OBSERVER-side
|
|
103
|
+
* vocabulary. It is a retained predecessor authority owned by Plan 01, so the
|
|
104
|
+
* two are bound by an executable set-equality assertion in the app suite rather
|
|
105
|
+
* than by one of them silently forking.
|
|
106
|
+
*/
|
|
107
|
+
export const REFILL_V3_WORLD_STATE_CACHE_OUTCOMES = [
|
|
108
|
+
"not_checked",
|
|
109
|
+
"not_eligible",
|
|
110
|
+
"fresh_cache_used",
|
|
111
|
+
"refresh_started",
|
|
112
|
+
"refreshed",
|
|
113
|
+
"refresh_in_progress",
|
|
114
|
+
"refresh_failed",
|
|
115
|
+
"uncertain",
|
|
116
|
+
];
|
|
117
|
+
export const REFILL_V3_WORLD_STATE_PRODUCT_EFFECT_KEYS = [
|
|
118
|
+
"scheduled",
|
|
119
|
+
"placed",
|
|
120
|
+
"sent",
|
|
121
|
+
"campaignMutated",
|
|
122
|
+
"approved",
|
|
123
|
+
"enriched",
|
|
124
|
+
"generated",
|
|
125
|
+
"salesNavigatorActioned",
|
|
126
|
+
];
|
|
127
|
+
/**
|
|
128
|
+
* Policy and identity classes the public result must never carry. `leaves`,
|
|
129
|
+
* `campaignActionFacts`, and `possibleActions` are internal implementation
|
|
130
|
+
* terms; raw row/cell/table/action identities are fetched just in time by the
|
|
131
|
+
* diagnostic authority; and graph/rank/selection fields would move backend
|
|
132
|
+
* authority into the caller.
|
|
133
|
+
*/
|
|
134
|
+
export const REFILL_V3_WORLD_STATE_FORBIDDEN_LEAK_KEYS = [
|
|
135
|
+
"campaignActionFacts",
|
|
136
|
+
"leaves",
|
|
137
|
+
"possibleActions",
|
|
138
|
+
"boundedRowIds",
|
|
139
|
+
"rowIds",
|
|
140
|
+
"rowId",
|
|
141
|
+
"cellId",
|
|
142
|
+
"cellIds",
|
|
143
|
+
"tableId",
|
|
144
|
+
"actionId",
|
|
145
|
+
"actionIds",
|
|
146
|
+
"actionColumnId",
|
|
147
|
+
"graph",
|
|
148
|
+
"rank",
|
|
149
|
+
"ranking",
|
|
150
|
+
"selectedCampaign",
|
|
151
|
+
"waterfall",
|
|
152
|
+
"queryDebug",
|
|
153
|
+
];
|
|
154
|
+
/** Every product mutation boolean, reported explicitly rather than omitted. */
|
|
155
|
+
export const REFILL_V3_WORLD_STATE_ZERO_PRODUCT_EFFECTS = Object.freeze({
|
|
156
|
+
scheduled: false,
|
|
157
|
+
placed: false,
|
|
158
|
+
sent: false,
|
|
159
|
+
campaignMutated: false,
|
|
160
|
+
approved: false,
|
|
161
|
+
enriched: false,
|
|
162
|
+
generated: false,
|
|
163
|
+
salesNavigatorActioned: false,
|
|
164
|
+
});
|
|
165
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
166
|
+
// Public output schema
|
|
167
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
168
|
+
const REASON_SCHEMA = {
|
|
169
|
+
type: ["object", "null"],
|
|
170
|
+
additionalProperties: false,
|
|
171
|
+
required: ["kind", "codes", "message"],
|
|
172
|
+
properties: {
|
|
173
|
+
kind: { type: "string", enum: [...REFILL_V3_WORLD_STATE_REASON_KINDS] },
|
|
174
|
+
codes: {
|
|
175
|
+
type: "array",
|
|
176
|
+
items: { type: "string", enum: [...REFILL_V3_WORLD_STATE_REASON_CODES] },
|
|
177
|
+
},
|
|
178
|
+
message: { type: "string" },
|
|
179
|
+
resumeAt: { type: "string" },
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
const CAMPAIGN_SCHEMA = {
|
|
183
|
+
type: "object",
|
|
184
|
+
additionalProperties: false,
|
|
185
|
+
required: [
|
|
186
|
+
"id",
|
|
187
|
+
"name",
|
|
188
|
+
"status",
|
|
189
|
+
"lanes",
|
|
190
|
+
"eligible",
|
|
191
|
+
"reason",
|
|
192
|
+
"startEligible",
|
|
193
|
+
"activeJob",
|
|
194
|
+
"supply",
|
|
195
|
+
"leadSourceFamily",
|
|
196
|
+
"evidence",
|
|
197
|
+
],
|
|
198
|
+
properties: {
|
|
199
|
+
id: { type: "string", minLength: 1 },
|
|
200
|
+
name: { type: "string" },
|
|
201
|
+
status: {
|
|
202
|
+
type: "string",
|
|
203
|
+
enum: [...REFILL_V3_WORLD_STATE_CAMPAIGN_STATUSES],
|
|
204
|
+
},
|
|
205
|
+
lanes: {
|
|
206
|
+
type: "array",
|
|
207
|
+
minItems: 1,
|
|
208
|
+
items: { type: "string", enum: [...REFILL_V3_WORLD_STATE_LANES] },
|
|
209
|
+
},
|
|
210
|
+
eligible: { type: "boolean" },
|
|
211
|
+
reason: REASON_SCHEMA,
|
|
212
|
+
startEligible: { type: "boolean" },
|
|
213
|
+
activeJob: {
|
|
214
|
+
type: ["object", "null"],
|
|
215
|
+
additionalProperties: false,
|
|
216
|
+
required: ["id", "kind", "status"],
|
|
217
|
+
properties: {
|
|
218
|
+
id: { type: "string", minLength: 1 },
|
|
219
|
+
kind: { type: "string", enum: [...REFILL_V3_ACTIVE_JOB_KINDS] },
|
|
220
|
+
status: { type: "string", enum: [...REFILL_V3_ACTIVE_JOB_STATUSES] },
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
supply: {
|
|
224
|
+
type: "object",
|
|
225
|
+
additionalProperties: false,
|
|
226
|
+
required: [...REFILL_V3_SUPPLY_BUCKETS],
|
|
227
|
+
properties: {
|
|
228
|
+
readyToSchedule: { type: "integer", minimum: 0 },
|
|
229
|
+
readyForApproval: { type: "integer", minimum: 0 },
|
|
230
|
+
readyForMessageGeneration: { type: "integer", minimum: 0 },
|
|
231
|
+
needsEnrichment: { type: "integer", minimum: 0 },
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
leadSourceFamily: {
|
|
235
|
+
type: "string",
|
|
236
|
+
enum: [...REFILL_V3_LEAD_SOURCE_FAMILIES],
|
|
237
|
+
},
|
|
238
|
+
evidence: {
|
|
239
|
+
type: "object",
|
|
240
|
+
additionalProperties: false,
|
|
241
|
+
required: [
|
|
242
|
+
"evergreen",
|
|
243
|
+
"lastActualUseAt",
|
|
244
|
+
"lastScheduledUseAt",
|
|
245
|
+
"effectiveness",
|
|
246
|
+
"supplyHealth",
|
|
247
|
+
],
|
|
248
|
+
properties: {
|
|
249
|
+
evergreen: { type: "boolean" },
|
|
250
|
+
lastActualUseAt: { type: ["string", "null"] },
|
|
251
|
+
lastScheduledUseAt: { type: ["string", "null"] },
|
|
252
|
+
effectiveness: {
|
|
253
|
+
type: "string",
|
|
254
|
+
enum: [...REFILL_V3_WORLD_STATE_EFFECTIVENESS],
|
|
255
|
+
},
|
|
256
|
+
supplyHealth: {
|
|
257
|
+
type: "string",
|
|
258
|
+
enum: [...REFILL_V3_WORLD_STATE_SUPPLY_HEALTH],
|
|
259
|
+
},
|
|
260
|
+
// ADDITIVE OPTIONAL (146.5-12 §87): the real numbers behind the
|
|
261
|
+
// effectiveness band; deliberately NOT in `required` so older
|
|
262
|
+
// backends stay schema-valid.
|
|
263
|
+
engagement: {
|
|
264
|
+
type: "object",
|
|
265
|
+
additionalProperties: false,
|
|
266
|
+
required: ["sends", "accepted", "replied", "acceptRate", "replyRate"],
|
|
267
|
+
properties: {
|
|
268
|
+
sends: { type: "number", minimum: 0 },
|
|
269
|
+
accepted: { type: "number", minimum: 0 },
|
|
270
|
+
replied: { type: "number", minimum: 0 },
|
|
271
|
+
acceptRate: { type: "number", minimum: 0, maximum: 100 },
|
|
272
|
+
replyRate: { type: "number", minimum: 0, maximum: 100 },
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
/**
|
|
280
|
+
* A TOP-LEVEL OBJECT, never an array: an MCP client validates
|
|
281
|
+
* `structuredContent` against this declaration, and a tool that declares an
|
|
282
|
+
* output schema and answers with a shape the client rejects has its ENTIRE
|
|
283
|
+
* response discarded.
|
|
284
|
+
*/
|
|
285
|
+
export const REFILL_V3_WORLD_STATE_OUTPUT_SCHEMA = Object.freeze({
|
|
286
|
+
type: "object",
|
|
287
|
+
additionalProperties: false,
|
|
288
|
+
required: ["rows", "durationMs", "observationSideEffects"],
|
|
289
|
+
properties: {
|
|
290
|
+
rows: {
|
|
291
|
+
type: "array",
|
|
292
|
+
items: {
|
|
293
|
+
type: "object",
|
|
294
|
+
additionalProperties: false,
|
|
295
|
+
required: [
|
|
296
|
+
"date",
|
|
297
|
+
"sender",
|
|
298
|
+
"lane",
|
|
299
|
+
"targetSlots",
|
|
300
|
+
"takenSlots",
|
|
301
|
+
"availableSlots",
|
|
302
|
+
"reason",
|
|
303
|
+
"activeCampaigns",
|
|
304
|
+
"inactiveCampaigns",
|
|
305
|
+
],
|
|
306
|
+
properties: {
|
|
307
|
+
date: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" },
|
|
308
|
+
sender: {
|
|
309
|
+
type: "object",
|
|
310
|
+
additionalProperties: false,
|
|
311
|
+
required: ["id", "name", "timezone"],
|
|
312
|
+
properties: {
|
|
313
|
+
id: { type: "string", minLength: 1 },
|
|
314
|
+
name: { type: "string" },
|
|
315
|
+
timezone: { type: "string", minLength: 1 },
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
lane: { type: "string", enum: [...REFILL_V3_WORLD_STATE_LANES] },
|
|
319
|
+
targetSlots: { type: "integer", minimum: 0 },
|
|
320
|
+
takenSlots: { type: "integer", minimum: 0 },
|
|
321
|
+
availableSlots: { type: "integer", minimum: 0 },
|
|
322
|
+
reason: REASON_SCHEMA,
|
|
323
|
+
activeCampaigns: { type: "array", items: CAMPAIGN_SCHEMA },
|
|
324
|
+
inactiveCampaigns: { type: "array", items: CAMPAIGN_SCHEMA },
|
|
325
|
+
// Additive optional (146.5-12 §84b): paid rows carry their credit
|
|
326
|
+
// numbers so a 0-slot paid lane explains itself.
|
|
327
|
+
paidCreditEvidence: {
|
|
328
|
+
type: ["object", "null"],
|
|
329
|
+
additionalProperties: false,
|
|
330
|
+
required: [
|
|
331
|
+
"availableCredits",
|
|
332
|
+
"minimumRequired",
|
|
333
|
+
"checkedAt",
|
|
334
|
+
"fresh",
|
|
335
|
+
],
|
|
336
|
+
properties: {
|
|
337
|
+
availableCredits: { type: ["integer", "null"], minimum: 0 },
|
|
338
|
+
minimumRequired: { type: ["integer", "null"], minimum: 0 },
|
|
339
|
+
checkedAt: { type: ["string", "null"] },
|
|
340
|
+
fresh: { type: "boolean" },
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
durationMs: { type: "number", minimum: 0 },
|
|
347
|
+
observationSideEffects: {
|
|
348
|
+
type: "object",
|
|
349
|
+
additionalProperties: false,
|
|
350
|
+
required: [
|
|
351
|
+
...REFILL_V3_WORLD_STATE_PRODUCT_EFFECT_KEYS,
|
|
352
|
+
"paidCreditCache",
|
|
353
|
+
],
|
|
354
|
+
properties: {
|
|
355
|
+
scheduled: { type: "boolean", enum: [false] },
|
|
356
|
+
placed: { type: "boolean", enum: [false] },
|
|
357
|
+
sent: { type: "boolean", enum: [false] },
|
|
358
|
+
campaignMutated: { type: "boolean", enum: [false] },
|
|
359
|
+
approved: { type: "boolean", enum: [false] },
|
|
360
|
+
enriched: { type: "boolean", enum: [false] },
|
|
361
|
+
generated: { type: "boolean", enum: [false] },
|
|
362
|
+
salesNavigatorActioned: { type: "boolean", enum: [false] },
|
|
363
|
+
paidCreditCache: {
|
|
364
|
+
type: "string",
|
|
365
|
+
enum: [...REFILL_V3_WORLD_STATE_CACHE_OUTCOMES],
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
372
|
+
// Decoder
|
|
373
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
374
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
375
|
+
// Decoder
|
|
376
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
377
|
+
/**
|
|
378
|
+
* A policy leak found ANYWHERE rejects the whole result, while an unrecognized
|
|
379
|
+
* enum value only drops the unit that carries it. The two outcomes therefore
|
|
380
|
+
* cannot share `null`.
|
|
381
|
+
*/
|
|
382
|
+
const LEAK = Symbol("refill_v3_world_state_policy_leak");
|
|
383
|
+
function isRecord(value) {
|
|
384
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
385
|
+
}
|
|
386
|
+
function isNonEmptyString(value) {
|
|
387
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
388
|
+
}
|
|
389
|
+
function isWholeNumber(value) {
|
|
390
|
+
return Number.isInteger(value) && value >= 0;
|
|
391
|
+
}
|
|
392
|
+
function isOneOf(value, options) {
|
|
393
|
+
return (typeof value === "string" && options.includes(value));
|
|
394
|
+
}
|
|
395
|
+
function isNullableString(value) {
|
|
396
|
+
return value === null || typeof value === "string";
|
|
397
|
+
}
|
|
398
|
+
function isRoundTripDate(value) {
|
|
399
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
const parsed = new Date(`${value}T00:00:00.000Z`);
|
|
403
|
+
return (!Number.isNaN(parsed.getTime()) &&
|
|
404
|
+
parsed.toISOString().slice(0, 10) === value);
|
|
405
|
+
}
|
|
406
|
+
function leaksPolicyKey(record) {
|
|
407
|
+
return REFILL_V3_WORLD_STATE_FORBIDDEN_LEAK_KEYS.some((key) => key in record);
|
|
408
|
+
}
|
|
409
|
+
function decodeReason(value) {
|
|
410
|
+
if (value === null)
|
|
411
|
+
return null;
|
|
412
|
+
if (!isRecord(value))
|
|
413
|
+
return null;
|
|
414
|
+
if (leaksPolicyKey(value))
|
|
415
|
+
return LEAK;
|
|
416
|
+
if (!isOneOf(value.kind, REFILL_V3_WORLD_STATE_REASON_KINDS) ||
|
|
417
|
+
!Array.isArray(value.codes) ||
|
|
418
|
+
!value.codes.every((code) => isOneOf(code, REFILL_V3_WORLD_STATE_REASON_CODES)) ||
|
|
419
|
+
typeof value.message !== "string" ||
|
|
420
|
+
!(value.resumeAt === undefined || typeof value.resumeAt === "string")) {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
const reason = {
|
|
424
|
+
kind: value.kind,
|
|
425
|
+
codes: [...value.codes],
|
|
426
|
+
message: value.message,
|
|
427
|
+
};
|
|
428
|
+
if (value.resumeAt !== undefined)
|
|
429
|
+
reason.resumeAt = value.resumeAt;
|
|
430
|
+
return reason;
|
|
431
|
+
}
|
|
432
|
+
function decodeActiveJob(value) {
|
|
433
|
+
if (value === null)
|
|
434
|
+
return { ok: true, value: null };
|
|
435
|
+
if (!isRecord(value))
|
|
436
|
+
return { ok: false };
|
|
437
|
+
if (leaksPolicyKey(value))
|
|
438
|
+
return LEAK;
|
|
439
|
+
if (!isNonEmptyString(value.id) ||
|
|
440
|
+
!isOneOf(value.kind, REFILL_V3_ACTIVE_JOB_KINDS) ||
|
|
441
|
+
!isOneOf(value.status, REFILL_V3_ACTIVE_JOB_STATUSES)) {
|
|
442
|
+
return { ok: false };
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
ok: true,
|
|
446
|
+
value: { id: value.id, kind: value.kind, status: value.status },
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function decodeSupply(value) {
|
|
450
|
+
if (!isRecord(value))
|
|
451
|
+
return null;
|
|
452
|
+
if (leaksPolicyKey(value))
|
|
453
|
+
return LEAK;
|
|
454
|
+
for (const bucket of REFILL_V3_SUPPLY_BUCKETS) {
|
|
455
|
+
if (!isWholeNumber(value[bucket]))
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
readyToSchedule: value.readyToSchedule,
|
|
460
|
+
readyForApproval: value.readyForApproval,
|
|
461
|
+
readyForMessageGeneration: value.readyForMessageGeneration,
|
|
462
|
+
needsEnrichment: value.needsEnrichment,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function decodeCampaignEvidence(value) {
|
|
466
|
+
if (!isRecord(value))
|
|
467
|
+
return null;
|
|
468
|
+
if (leaksPolicyKey(value))
|
|
469
|
+
return LEAK;
|
|
470
|
+
if (typeof value.evergreen !== "boolean" ||
|
|
471
|
+
!isNullableString(value.lastActualUseAt) ||
|
|
472
|
+
!isNullableString(value.lastScheduledUseAt) ||
|
|
473
|
+
!isOneOf(value.effectiveness, REFILL_V3_WORLD_STATE_EFFECTIVENESS) ||
|
|
474
|
+
!isOneOf(value.supplyHealth, REFILL_V3_WORLD_STATE_SUPPLY_HEALTH)) {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
// ADDITIVE decode (146.5-12 §87): absent stays absent (older backend);
|
|
478
|
+
// a PRESENT engagement must decode strictly or the campaign drops —
|
|
479
|
+
// numbers are evidence, and malformed evidence is never half-kept.
|
|
480
|
+
let engagement;
|
|
481
|
+
if (value.engagement !== undefined && value.engagement !== null) {
|
|
482
|
+
const decoded = decodeCampaignEngagement(value.engagement);
|
|
483
|
+
if (decoded === null)
|
|
484
|
+
return null;
|
|
485
|
+
engagement = decoded;
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
evergreen: value.evergreen,
|
|
489
|
+
lastActualUseAt: value.lastActualUseAt,
|
|
490
|
+
lastScheduledUseAt: value.lastScheduledUseAt,
|
|
491
|
+
effectiveness: value.effectiveness,
|
|
492
|
+
supplyHealth: value.supplyHealth,
|
|
493
|
+
...(engagement ? { engagement } : {}),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function decodeCampaignEngagement(value) {
|
|
497
|
+
if (!isRecord(value))
|
|
498
|
+
return null;
|
|
499
|
+
const isCount = (entry) => typeof entry === "number" && Number.isFinite(entry) && entry >= 0;
|
|
500
|
+
if (!isCount(value.sends) ||
|
|
501
|
+
!isCount(value.accepted) ||
|
|
502
|
+
!isCount(value.replied) ||
|
|
503
|
+
!isCount(value.acceptRate) ||
|
|
504
|
+
!isCount(value.replyRate) ||
|
|
505
|
+
value.acceptRate > 100 ||
|
|
506
|
+
value.replyRate > 100) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
sends: value.sends,
|
|
511
|
+
accepted: value.accepted,
|
|
512
|
+
replied: value.replied,
|
|
513
|
+
acceptRate: value.acceptRate,
|
|
514
|
+
replyRate: value.replyRate,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
/** `null` drops only this campaign entry; `LEAK` rejects the whole result. */
|
|
518
|
+
function decodeCampaign(value) {
|
|
519
|
+
if (!isRecord(value))
|
|
520
|
+
return null;
|
|
521
|
+
if (leaksPolicyKey(value))
|
|
522
|
+
return LEAK;
|
|
523
|
+
if (!isNonEmptyString(value.id) ||
|
|
524
|
+
typeof value.name !== "string" ||
|
|
525
|
+
!isOneOf(value.status, REFILL_V3_WORLD_STATE_CAMPAIGN_STATUSES) ||
|
|
526
|
+
!Array.isArray(value.lanes) ||
|
|
527
|
+
value.lanes.length === 0 ||
|
|
528
|
+
!value.lanes.every((lane) => isOneOf(lane, REFILL_V3_WORLD_STATE_LANES)) ||
|
|
529
|
+
typeof value.eligible !== "boolean" ||
|
|
530
|
+
typeof value.startEligible !== "boolean" ||
|
|
531
|
+
!isOneOf(value.leadSourceFamily, REFILL_V3_LEAD_SOURCE_FAMILIES)) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
const reason = decodeReason(value.reason);
|
|
535
|
+
if (reason === LEAK)
|
|
536
|
+
return LEAK;
|
|
537
|
+
if (value.reason !== null && reason === null)
|
|
538
|
+
return null;
|
|
539
|
+
const activeJob = decodeActiveJob(value.activeJob);
|
|
540
|
+
if (activeJob === LEAK)
|
|
541
|
+
return LEAK;
|
|
542
|
+
if (!activeJob.ok)
|
|
543
|
+
return null;
|
|
544
|
+
const supply = decodeSupply(value.supply);
|
|
545
|
+
if (supply === LEAK)
|
|
546
|
+
return LEAK;
|
|
547
|
+
if (supply === null)
|
|
548
|
+
return null;
|
|
549
|
+
const evidence = decodeCampaignEvidence(value.evidence);
|
|
550
|
+
if (evidence === LEAK)
|
|
551
|
+
return LEAK;
|
|
552
|
+
if (evidence === null)
|
|
553
|
+
return null;
|
|
554
|
+
return {
|
|
555
|
+
id: value.id,
|
|
556
|
+
name: value.name,
|
|
557
|
+
status: value.status,
|
|
558
|
+
lanes: [...value.lanes],
|
|
559
|
+
eligible: value.eligible,
|
|
560
|
+
reason,
|
|
561
|
+
startEligible: value.startEligible,
|
|
562
|
+
activeJob: activeJob.value,
|
|
563
|
+
supply,
|
|
564
|
+
leadSourceFamily: value.leadSourceFamily,
|
|
565
|
+
evidence,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function decodeCampaignList(value) {
|
|
569
|
+
if (!Array.isArray(value))
|
|
570
|
+
return null;
|
|
571
|
+
const options = [];
|
|
572
|
+
for (const entry of value) {
|
|
573
|
+
const option = decodeCampaign(entry);
|
|
574
|
+
if (option === LEAK)
|
|
575
|
+
return LEAK;
|
|
576
|
+
if (option === null)
|
|
577
|
+
continue;
|
|
578
|
+
options.push(option);
|
|
579
|
+
}
|
|
580
|
+
return options;
|
|
581
|
+
}
|
|
582
|
+
function decodeSender(value) {
|
|
583
|
+
if (!isRecord(value))
|
|
584
|
+
return null;
|
|
585
|
+
if (leaksPolicyKey(value))
|
|
586
|
+
return LEAK;
|
|
587
|
+
if (!isNonEmptyString(value.id) ||
|
|
588
|
+
typeof value.name !== "string" ||
|
|
589
|
+
!isNonEmptyString(value.timezone)) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
return { id: value.id, name: value.name, timezone: value.timezone };
|
|
593
|
+
}
|
|
594
|
+
/** `null` drops only this row; `LEAK` rejects the whole result. */
|
|
595
|
+
function decodeRow(value) {
|
|
596
|
+
if (!isRecord(value))
|
|
597
|
+
return null;
|
|
598
|
+
if (leaksPolicyKey(value))
|
|
599
|
+
return LEAK;
|
|
600
|
+
if (!isRoundTripDate(value.date) ||
|
|
601
|
+
!isOneOf(value.lane, REFILL_V3_WORLD_STATE_LANES) ||
|
|
602
|
+
!isWholeNumber(value.targetSlots) ||
|
|
603
|
+
!isWholeNumber(value.takenSlots) ||
|
|
604
|
+
!isWholeNumber(value.availableSlots)) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
const remaining = Math.max(value.targetSlots - value.takenSlots, 0);
|
|
608
|
+
if (value.availableSlots > remaining)
|
|
609
|
+
return null;
|
|
610
|
+
const sender = decodeSender(value.sender);
|
|
611
|
+
if (sender === LEAK)
|
|
612
|
+
return LEAK;
|
|
613
|
+
if (sender === null)
|
|
614
|
+
return null;
|
|
615
|
+
const reason = decodeReason(value.reason);
|
|
616
|
+
if (reason === LEAK)
|
|
617
|
+
return LEAK;
|
|
618
|
+
if (value.reason !== null && reason === null)
|
|
619
|
+
return null;
|
|
620
|
+
const activeCampaigns = decodeCampaignList(value.activeCampaigns);
|
|
621
|
+
if (activeCampaigns === LEAK)
|
|
622
|
+
return LEAK;
|
|
623
|
+
if (activeCampaigns === null)
|
|
624
|
+
return null;
|
|
625
|
+
const inactiveCampaigns = decodeCampaignList(value.inactiveCampaigns);
|
|
626
|
+
if (inactiveCampaigns === LEAK)
|
|
627
|
+
return LEAK;
|
|
628
|
+
if (inactiveCampaigns === null)
|
|
629
|
+
return null;
|
|
630
|
+
const paidCreditEvidence = decodePaidCreditEvidence(value.paidCreditEvidence);
|
|
631
|
+
if (paidCreditEvidence === INVALID_CREDIT_EVIDENCE)
|
|
632
|
+
return null;
|
|
633
|
+
return {
|
|
634
|
+
date: value.date,
|
|
635
|
+
sender,
|
|
636
|
+
lane: value.lane,
|
|
637
|
+
targetSlots: value.targetSlots,
|
|
638
|
+
takenSlots: value.takenSlots,
|
|
639
|
+
availableSlots: value.availableSlots,
|
|
640
|
+
reason,
|
|
641
|
+
activeCampaigns,
|
|
642
|
+
inactiveCampaigns,
|
|
643
|
+
...(paidCreditEvidence === undefined ? {} : { paidCreditEvidence }),
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
const INVALID_CREDIT_EVIDENCE = Symbol("invalid-credit-evidence");
|
|
647
|
+
/**
|
|
648
|
+
* Additive optional decode (146.5-12 §84b): absent stays absent (older
|
|
649
|
+
* backends), null stays null, a well-formed object round-trips, and a
|
|
650
|
+
* malformed one drops the row rather than fabricating numbers.
|
|
651
|
+
*/
|
|
652
|
+
function decodePaidCreditEvidence(value) {
|
|
653
|
+
if (value === undefined)
|
|
654
|
+
return undefined;
|
|
655
|
+
if (value === null)
|
|
656
|
+
return null;
|
|
657
|
+
if (!isRecord(value))
|
|
658
|
+
return INVALID_CREDIT_EVIDENCE;
|
|
659
|
+
const availableOk = value.availableCredits === null || isWholeNumber(value.availableCredits);
|
|
660
|
+
const minimumOk = value.minimumRequired === null || isWholeNumber(value.minimumRequired);
|
|
661
|
+
const checkedOk = value.checkedAt === null || typeof value.checkedAt === "string";
|
|
662
|
+
if (!availableOk || !minimumOk || !checkedOk) {
|
|
663
|
+
return INVALID_CREDIT_EVIDENCE;
|
|
664
|
+
}
|
|
665
|
+
if (typeof value.fresh !== "boolean")
|
|
666
|
+
return INVALID_CREDIT_EVIDENCE;
|
|
667
|
+
return {
|
|
668
|
+
availableCredits: value.availableCredits,
|
|
669
|
+
minimumRequired: value.minimumRequired,
|
|
670
|
+
checkedAt: value.checkedAt,
|
|
671
|
+
fresh: value.fresh,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function decodeSideEffects(value) {
|
|
675
|
+
if (!isRecord(value))
|
|
676
|
+
return null;
|
|
677
|
+
if (leaksPolicyKey(value))
|
|
678
|
+
return LEAK;
|
|
679
|
+
for (const key of REFILL_V3_WORLD_STATE_PRODUCT_EFFECT_KEYS) {
|
|
680
|
+
if (value[key] !== false)
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
if (typeof value.paidCreditCache !== "string")
|
|
684
|
+
return null;
|
|
685
|
+
return {
|
|
686
|
+
...REFILL_V3_WORLD_STATE_ZERO_PRODUCT_EFFECTS,
|
|
687
|
+
// An unrecognized outcome is not a reason to fail an installed client
|
|
688
|
+
// closed. `uncertain` is the vocabulary's own "we do not know" member, so
|
|
689
|
+
// the honest projection preserves every row instead of blanking them.
|
|
690
|
+
paidCreditCache: isOneOf(value.paidCreditCache, REFILL_V3_WORLD_STATE_CACHE_OUTCOMES)
|
|
691
|
+
? value.paidCreditCache
|
|
692
|
+
: "uncertain",
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Sole unknown-value decoder for the public result.
|
|
697
|
+
*
|
|
698
|
+
* Strict inbound, tolerant outbound, lossless. `null` means "not this
|
|
699
|
+
* contract"; the typed unsupported VALUE means "a major this build cannot
|
|
700
|
+
* speak". Collapsing the second into the first, or into business zero, is the
|
|
701
|
+
* defect this separation exists to prevent.
|
|
702
|
+
*/
|
|
703
|
+
export function decodeRefillWorldStateResult(value) {
|
|
704
|
+
if (!isRecord(value))
|
|
705
|
+
return null;
|
|
706
|
+
const support = resolveRefillV3SchemaVersion(value.schemaVersion, REFILL_V3_WORLD_STATE_SCHEMA_NAMESPACE, REFILL_V3_WORLD_STATE_SCHEMA_MAJOR);
|
|
707
|
+
if (!support.supported) {
|
|
708
|
+
return support.reason === REFILL_V3_SCHEMA_VERSION_UNSUPPORTED
|
|
709
|
+
? REFILL_V3_SCHEMA_VERSION_UNSUPPORTED
|
|
710
|
+
: null;
|
|
711
|
+
}
|
|
712
|
+
if (leaksPolicyKey(value))
|
|
713
|
+
return null;
|
|
714
|
+
if (!Array.isArray(value.rows))
|
|
715
|
+
return null;
|
|
716
|
+
if (typeof value.durationMs !== "number" ||
|
|
717
|
+
!Number.isFinite(value.durationMs) ||
|
|
718
|
+
value.durationMs < 0) {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
const observationSideEffects = decodeSideEffects(value.observationSideEffects);
|
|
722
|
+
if (observationSideEffects === LEAK || observationSideEffects === null) {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
const rows = [];
|
|
726
|
+
for (const entry of value.rows) {
|
|
727
|
+
const row = decodeRow(entry);
|
|
728
|
+
if (row === LEAK)
|
|
729
|
+
return null;
|
|
730
|
+
if (row === null)
|
|
731
|
+
continue;
|
|
732
|
+
rows.push(row);
|
|
733
|
+
}
|
|
734
|
+
return { rows, durationMs: value.durationMs, observationSideEffects };
|
|
735
|
+
}
|
|
736
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
737
|
+
// Encoder
|
|
738
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
739
|
+
function encodeReason(reason) {
|
|
740
|
+
if (reason === null)
|
|
741
|
+
return null;
|
|
742
|
+
const encoded = {
|
|
743
|
+
kind: reason.kind,
|
|
744
|
+
codes: [...reason.codes],
|
|
745
|
+
message: reason.message,
|
|
746
|
+
};
|
|
747
|
+
if (reason.resumeAt !== undefined)
|
|
748
|
+
encoded.resumeAt = reason.resumeAt;
|
|
749
|
+
return encoded;
|
|
750
|
+
}
|
|
751
|
+
function encodeCampaign(option) {
|
|
752
|
+
return {
|
|
753
|
+
id: option.id,
|
|
754
|
+
name: option.name,
|
|
755
|
+
status: option.status,
|
|
756
|
+
lanes: [...option.lanes],
|
|
757
|
+
eligible: option.eligible,
|
|
758
|
+
reason: encodeReason(option.reason),
|
|
759
|
+
startEligible: option.startEligible,
|
|
760
|
+
activeJob: option.activeJob,
|
|
761
|
+
supply: { ...option.supply },
|
|
762
|
+
leadSourceFamily: option.leadSourceFamily,
|
|
763
|
+
evidence: { ...option.evidence },
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Wire form of a decoded result. `schemaVersion` is TRANSPORT, not business:
|
|
768
|
+
* the canonical result stays exactly `{rows, durationMs,
|
|
769
|
+
* observationSideEffects}`. Every locked field is projected explicitly, because
|
|
770
|
+
* a silently dropped property is what turns a round trip into a confident wrong
|
|
771
|
+
* success.
|
|
772
|
+
*/
|
|
773
|
+
export function encodeRefillWorldStateResult(result) {
|
|
774
|
+
return {
|
|
775
|
+
schemaVersion: REFILL_V3_WORLD_STATE_SCHEMA_VERSION,
|
|
776
|
+
rows: result.rows.map((row) => ({
|
|
777
|
+
date: row.date,
|
|
778
|
+
sender: { ...row.sender },
|
|
779
|
+
lane: row.lane,
|
|
780
|
+
targetSlots: row.targetSlots,
|
|
781
|
+
takenSlots: row.takenSlots,
|
|
782
|
+
availableSlots: row.availableSlots,
|
|
783
|
+
reason: encodeReason(row.reason),
|
|
784
|
+
activeCampaigns: row.activeCampaigns.map(encodeCampaign),
|
|
785
|
+
inactiveCampaigns: row.inactiveCampaigns.map(encodeCampaign),
|
|
786
|
+
})),
|
|
787
|
+
durationMs: result.durationMs,
|
|
788
|
+
observationSideEffects: { ...result.observationSideEffects },
|
|
789
|
+
};
|
|
790
|
+
}
|