@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.
Files changed (54) hide show
  1. package/dist/agent-tool-names.d.ts +2 -2
  2. package/dist/agent-tool-names.js +14 -0
  3. package/dist/scheduler-envelope.d.ts +190 -0
  4. package/dist/scheduler-envelope.js +412 -0
  5. package/dist/server.js +28 -2
  6. package/dist/tools/campaign-message-preparation.d.ts +25 -0
  7. package/dist/tools/campaign-message-preparation.js +37 -0
  8. package/dist/tools/campaign-processing.d.ts +74 -1
  9. package/dist/tools/campaign-processing.js +57 -1
  10. package/dist/tools/prompts.d.ts +15 -3
  11. package/dist/tools/prompts.js +25 -0
  12. package/dist/tools/refill-executors.d.ts +43 -70
  13. package/dist/tools/refill-executors.js +201 -273
  14. package/dist/tools/refill-sends.d.ts +3 -3
  15. package/dist/tools/refill-sends.js +1 -1
  16. package/dist/tools/refill-target-plan.js +1 -1
  17. package/dist/tools/refill-v3-advance-contract.d.ts +618 -0
  18. package/dist/tools/refill-v3-advance-contract.js +920 -0
  19. package/dist/tools/refill-v3-advance.d.ts +397 -0
  20. package/dist/tools/refill-v3-advance.js +75 -0
  21. package/dist/tools/refill-v3-attention-packet-contract.d.ts +608 -0
  22. package/dist/tools/refill-v3-attention-packet-contract.js +911 -0
  23. package/dist/tools/refill-v3-campaign-work.d.ts +324 -0
  24. package/dist/tools/refill-v3-campaign-work.js +1015 -0
  25. package/dist/tools/refill-v3-continue-contract.d.ts +433 -0
  26. package/dist/tools/refill-v3-continue-contract.js +112 -0
  27. package/dist/tools/refill-v3-continue.d.ts +501 -0
  28. package/dist/tools/refill-v3-continue.js +654 -0
  29. package/dist/tools/refill-v3-edit-selected-campaign-contract.d.ts +208 -0
  30. package/dist/tools/refill-v3-edit-selected-campaign-contract.js +351 -0
  31. package/dist/tools/refill-v3-edit-selected-campaign.d.ts +232 -0
  32. package/dist/tools/refill-v3-edit-selected-campaign.js +147 -0
  33. package/dist/tools/refill-v3-fill-ready-contract.d.ts +198 -0
  34. package/dist/tools/refill-v3-fill-ready-contract.js +324 -0
  35. package/dist/tools/refill-v3-fill-ready.d.ts +199 -0
  36. package/dist/tools/refill-v3-fill-ready.js +136 -0
  37. package/dist/tools/refill-v3-source-family-dispatch.d.ts +249 -0
  38. package/dist/tools/refill-v3-source-family-dispatch.js +514 -0
  39. package/dist/tools/refill-v3-waterfall-contract.d.ts +156 -0
  40. package/dist/tools/refill-v3-waterfall-contract.js +181 -0
  41. package/dist/tools/refill-v3-waterfall.d.ts +136 -0
  42. package/dist/tools/refill-v3-waterfall.js +161 -0
  43. package/dist/tools/refill-v3-world-state-contract.d.ts +666 -0
  44. package/dist/tools/refill-v3-world-state-contract.js +790 -0
  45. package/dist/tools/refill-v3-world-state.d.ts +592 -0
  46. package/dist/tools/refill-v3-world-state.js +137 -0
  47. package/dist/tools/registry.d.ts +1710 -3
  48. package/dist/tools/registry.js +19 -0
  49. package/package.json +1 -1
  50. package/skills/refill-sends/SKILL.md +89 -4
  51. package/skills/refill-sends-waterfall-order/SKILL.md +174 -0
  52. package/skills/refill-sends-work-campaign/SKILL.md +253 -0
  53. package/skills/refill-sends-workflow/SKILL.md +112 -5
  54. 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
+ }