@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,911 @@
1
+ /**
2
+ * Refill V3 `campaign_attention_required` PACKAGE seam.
3
+ *
4
+ * This module is declared inside `@sellable/mcp` so the packet contract ships
5
+ * with the published package and both later waves import ONE declaration:
6
+ * the wave-7 AI seams validate their output against it, and the wave-8
7
+ * deterministic coordinator produces packets against it. Copying the packet
8
+ * shape, the action list, the exhaustion table, or the yield floor into either
9
+ * consumer is forbidden.
10
+ *
11
+ * It is also the phase's shared PACKAGE VOCABULARY owner. The campaign supply
12
+ * buckets, live-job vocabulary, lead-source families, schema-version gate, and
13
+ * the emitted-module registry are declared exactly once here and imported by
14
+ * the sibling world-state and waterfall contracts, because a vocabulary
15
+ * mirrored across two modules is a defect by construction (error law 2).
16
+ *
17
+ * Constraints:
18
+ * - zero imports: no app `src/**`, no validation library, no runtime dependency
19
+ * beyond what `@sellable/mcp` already publishes (assumption A02-NO-ZOD);
20
+ * - hand-rolled type guards plus as-const JSON-Schema literals;
21
+ * - strict-in, tolerant-out, lossless decoding; and
22
+ * - a compatible-RANGE schema version gate, never an exact-string pin.
23
+ */
24
+ /**
25
+ * Every package contract module pair this phase emits under `dist/tools/`.
26
+ * Earlier plans append their own module; Plan 10's single packed-asset
27
+ * assertion consumes this list instead of four escalating set-completeness
28
+ * assertions.
29
+ */
30
+ export const REFILL_V3_PACKAGED_CONTRACT_MODULES = [
31
+ {
32
+ source: "mcp/sellable/src/tools/refill-v3-world-state-contract.ts",
33
+ js: "dist/tools/refill-v3-world-state-contract.js",
34
+ types: "dist/tools/refill-v3-world-state-contract.d.ts",
35
+ ownerPlan: "146.5-02",
36
+ ownerTask: "146.5-02-T1",
37
+ },
38
+ {
39
+ source: "mcp/sellable/src/tools/refill-v3-attention-packet-contract.ts",
40
+ js: "dist/tools/refill-v3-attention-packet-contract.js",
41
+ types: "dist/tools/refill-v3-attention-packet-contract.d.ts",
42
+ ownerPlan: "146.5-02",
43
+ ownerTask: "146.5-02-T3",
44
+ },
45
+ {
46
+ source: "mcp/sellable/src/tools/refill-v3-waterfall-contract.ts",
47
+ js: "dist/tools/refill-v3-waterfall-contract.js",
48
+ types: "dist/tools/refill-v3-waterfall-contract.d.ts",
49
+ ownerPlan: "146.5-02",
50
+ ownerTask: "146.5-02-T3",
51
+ },
52
+ {
53
+ source: "mcp/sellable/src/tools/refill-v3-fill-ready-contract.ts",
54
+ js: "dist/tools/refill-v3-fill-ready-contract.js",
55
+ types: "dist/tools/refill-v3-fill-ready-contract.d.ts",
56
+ ownerPlan: "146.5-07",
57
+ ownerTask: "146.5-07-T3",
58
+ },
59
+ {
60
+ source: "mcp/sellable/src/tools/refill-v3-world-state.ts",
61
+ js: "dist/tools/refill-v3-world-state.js",
62
+ types: "dist/tools/refill-v3-world-state.d.ts",
63
+ ownerPlan: "146.5-08",
64
+ ownerTask: "146.5-08-T1",
65
+ },
66
+ {
67
+ source: "mcp/sellable/src/tools/refill-v3-fill-ready.ts",
68
+ js: "dist/tools/refill-v3-fill-ready.js",
69
+ types: "dist/tools/refill-v3-fill-ready.d.ts",
70
+ ownerPlan: "146.5-08",
71
+ ownerTask: "146.5-08-T2",
72
+ },
73
+ {
74
+ source: "mcp/sellable/src/tools/refill-v3-edit-selected-campaign-contract.ts",
75
+ js: "dist/tools/refill-v3-edit-selected-campaign-contract.js",
76
+ types: "dist/tools/refill-v3-edit-selected-campaign-contract.d.ts",
77
+ ownerPlan: "146.5-09",
78
+ ownerTask: "146.5-09-T2",
79
+ },
80
+ {
81
+ /**
82
+ * Plan 09's WRAPPER. Its contract pair was already registered; the wrapper
83
+ * itself was missing, which left the four-tool package parity claim
84
+ * unsatisfiable. Plan 10 T2 appended it because it owns set completeness
85
+ * across every emitted Refill V3 module.
86
+ */
87
+ source: "mcp/sellable/src/tools/refill-v3-edit-selected-campaign.ts",
88
+ js: "dist/tools/refill-v3-edit-selected-campaign.js",
89
+ types: "dist/tools/refill-v3-edit-selected-campaign.d.ts",
90
+ ownerPlan: "146.5-09",
91
+ ownerTask: "146.5-09-T2",
92
+ },
93
+ {
94
+ source: "mcp/sellable/src/tools/refill-v3-advance-contract.ts",
95
+ js: "dist/tools/refill-v3-advance-contract.js",
96
+ types: "dist/tools/refill-v3-advance-contract.d.ts",
97
+ ownerPlan: "146.5-10",
98
+ ownerTask: "146.5-10-T1a",
99
+ },
100
+ {
101
+ source: "mcp/sellable/src/tools/refill-v3-advance.ts",
102
+ js: "dist/tools/refill-v3-advance.js",
103
+ types: "dist/tools/refill-v3-advance.d.ts",
104
+ ownerPlan: "146.5-10",
105
+ ownerTask: "146.5-10-T1a",
106
+ },
107
+ {
108
+ source: "mcp/sellable/src/tools/refill-v3-continue-contract.ts",
109
+ js: "dist/tools/refill-v3-continue-contract.js",
110
+ types: "dist/tools/refill-v3-continue-contract.d.ts",
111
+ ownerPlan: "146.5-13",
112
+ ownerTask: "146.5-13-T2",
113
+ },
114
+ {
115
+ source: "mcp/sellable/src/tools/refill-v3-continue.ts",
116
+ js: "dist/tools/refill-v3-continue.js",
117
+ types: "dist/tools/refill-v3-continue.d.ts",
118
+ ownerPlan: "146.5-13",
119
+ ownerTask: "146.5-13-T2",
120
+ },
121
+ ];
122
+ // ───────────────────────────────────────────────────────────────────────────
123
+ // Shared schema-version gate
124
+ // ───────────────────────────────────────────────────────────────────────────
125
+ /**
126
+ * The typed VALUE an unsupported MAJOR returns. It is deliberately not `null`
127
+ * and never a throw: the backend deploys before the package is installed, so an
128
+ * already-installed client that fails closed on one new field would take every
129
+ * installed customer down (112-EMH-06).
130
+ */
131
+ export const REFILL_V3_SCHEMA_VERSION_UNSUPPORTED = "schema_version_unsupported";
132
+ /**
133
+ * Compatible-RANGE gate. Any minor of the supported major decodes, including a
134
+ * newer one; a different major returns the typed unsupported value; anything
135
+ * that is not `<namespace>.v<major>.<minor>` is malformed transport.
136
+ */
137
+ export function resolveRefillV3SchemaVersion(value, namespace, supportedMajor) {
138
+ if (typeof value !== "string")
139
+ return { supported: false, reason: "malformed" };
140
+ const prefix = `${namespace}.v`;
141
+ if (!value.startsWith(prefix))
142
+ return { supported: false, reason: "malformed" };
143
+ const parts = value.slice(prefix.length).split(".");
144
+ if (parts.length !== 2)
145
+ return { supported: false, reason: "malformed" };
146
+ const major = Number(parts[0]);
147
+ const minor = Number(parts[1]);
148
+ if (!Number.isInteger(major) ||
149
+ !Number.isInteger(minor) ||
150
+ major < 0 ||
151
+ minor < 0 ||
152
+ parts[0] === "" ||
153
+ parts[1] === "") {
154
+ return { supported: false, reason: "malformed" };
155
+ }
156
+ if (major !== supportedMajor) {
157
+ return { supported: false, reason: REFILL_V3_SCHEMA_VERSION_UNSUPPORTED };
158
+ }
159
+ return { supported: true, major, minor };
160
+ }
161
+ // ───────────────────────────────────────────────────────────────────────────
162
+ // Shared campaign fact vocabularies
163
+ // ───────────────────────────────────────────────────────────────────────────
164
+ export const REFILL_V3_LEAD_SOURCE_FAMILIES = [
165
+ "post_engager",
166
+ "signal_discovery",
167
+ "cold",
168
+ ];
169
+ export const REFILL_V3_ACTIVE_JOB_KINDS = [
170
+ "enrichment",
171
+ "message_generation",
172
+ "source_expansion",
173
+ "scheduling",
174
+ ];
175
+ export const REFILL_V3_ACTIVE_JOB_STATUSES = ["queued", "running"];
176
+ export const REFILL_V3_SUPPLY_BUCKETS = [
177
+ "readyToSchedule",
178
+ "readyForApproval",
179
+ "readyForMessageGeneration",
180
+ "needsEnrichment",
181
+ ];
182
+ /** The at-most-10 bounded diagnostic sample. A REFERENCE, never row payload. */
183
+ export const REFILL_V3_DIAGNOSTIC_SAMPLE_CAP = 10;
184
+ // ───────────────────────────────────────────────────────────────────────────
185
+ // Minimum source yield floor
186
+ // ───────────────────────────────────────────────────────────────────────────
187
+ /**
188
+ * The measured ACCEPTANCE FLOOR for a new source search: 8% of an evaluated
189
+ * sample must pass.
190
+ *
191
+ * This is NOT `SIGNAL_YIELD_DEFAULT_PASS_RATE` in `refill-executors.ts`, which
192
+ * is the no-evidence PROJECTION default used to size a request. Quality and
193
+ * volume stay separate on purpose: coupling a quality floor to gap-fill
194
+ * projection arithmetic is exactly what made the predecessor brittle.
195
+ */
196
+ export const REFILL_V3_MIN_SOURCE_YIELD = 0.08;
197
+ /** The acceptance sample is a fixed number of EVALUATED candidates. */
198
+ export const REFILL_V3_MIN_SOURCE_YIELD_SAMPLE = 20;
199
+ /** `post_engager` is excluded: it keeps posts-refresh preconditions, not a rate. */
200
+ export const REFILL_V3_YIELD_FLOOR_FAMILIES = [
201
+ "signal_discovery",
202
+ "cold",
203
+ ];
204
+ /**
205
+ * The DETERMINISTIC half of the yield floor. The AI judges WHICH sampled
206
+ * candidates pass; this owns sample-size enforcement, the arithmetic, the
207
+ * comparison, and the routing decision. There is deliberately no deterministic
208
+ * candidate judge here.
209
+ *
210
+ */
211
+ export function evaluateRefillV3SourceYield(evidence) {
212
+ if (!REFILL_V3_YIELD_FLOOR_FAMILIES.includes(evidence.family)) {
213
+ return { decision: "floor_not_applicable", family: evidence.family };
214
+ }
215
+ if (!Number.isInteger(evidence.evaluated) ||
216
+ !Number.isInteger(evidence.passes) ||
217
+ evidence.evaluated < 0 ||
218
+ evidence.passes < 0 ||
219
+ evidence.passes > evidence.evaluated) {
220
+ return {
221
+ decision: "invalid_evidence",
222
+ detail: `evaluated=${evidence.evaluated} passes=${evidence.passes} is not a whole-number sample with passes <= evaluated.`,
223
+ };
224
+ }
225
+ if (evidence.evaluated < REFILL_V3_MIN_SOURCE_YIELD_SAMPLE) {
226
+ return {
227
+ decision: "sample_incomplete",
228
+ evaluated: evidence.evaluated,
229
+ required: REFILL_V3_MIN_SOURCE_YIELD_SAMPLE,
230
+ };
231
+ }
232
+ const ratio = evidence.passes / evidence.evaluated;
233
+ return ratio >= REFILL_V3_MIN_SOURCE_YIELD
234
+ ? { decision: "meets_floor", ratio, floor: REFILL_V3_MIN_SOURCE_YIELD }
235
+ : { decision: "below_floor", ratio, floor: REFILL_V3_MIN_SOURCE_YIELD };
236
+ }
237
+ // ───────────────────────────────────────────────────────────────────────────
238
+ // Shared exhaustion predicate
239
+ // ───────────────────────────────────────────────────────────────────────────
240
+ export const REFILL_V3_EXHAUSTION_CONJUNCTS = [
241
+ "existing_supply_drained",
242
+ "no_live_active_job",
243
+ "no_repairable_existing_rows",
244
+ "no_undispatched_approved_work",
245
+ "source_frontier_scan_terminated",
246
+ ];
247
+ export const REFILL_V3_SCAN_TERMINATIONS = [
248
+ "not_started",
249
+ "in_progress",
250
+ "frontier_scanned",
251
+ "provider_unavailable",
252
+ ];
253
+ /**
254
+ * The ONE five-conjunct exhaustion predicate. Wave 7's AI falsification check
255
+ * and wave 8's coordinator both route through this symbol; declaring it in
256
+ * wave 8 would make a wave-7 behavior assert through an artifact that does not
257
+ * yet exist and would duplicate the same table twice.
258
+ *
259
+ * `structurallyUnactionableRows` and `retryExhaustedRows` are recorded evidence
260
+ * that SUPPORTS exhaustion; they can never falsify it, which is why they are
261
+ * inputs and not conjuncts.
262
+ */
263
+ export const REFILL_V3_EXHAUSTION_PREDICATE = {
264
+ conjuncts: REFILL_V3_EXHAUSTION_CONJUNCTS,
265
+ evaluate(input) {
266
+ const supplyRemaining = Object.keys(input.supply).some((bucket) => input.supply[bucket] > 0);
267
+ if (supplyRemaining) {
268
+ return { exhausted: false, falsifyingClause: "existing_supply_drained" };
269
+ }
270
+ if (input.activeJob !== null) {
271
+ return { exhausted: false, falsifyingClause: "no_live_active_job" };
272
+ }
273
+ if (input.repairableExistingRows > 0) {
274
+ return {
275
+ exhausted: false,
276
+ falsifyingClause: "no_repairable_existing_rows",
277
+ };
278
+ }
279
+ if (input.approvedUndispatchedWork > 0) {
280
+ return {
281
+ exhausted: false,
282
+ falsifyingClause: "no_undispatched_approved_work",
283
+ };
284
+ }
285
+ const productiveYield = input.yieldEvidence !== null &&
286
+ evaluateRefillV3SourceYield(input.yieldEvidence).decision ===
287
+ "meets_floor";
288
+ if (input.scanTermination !== "frontier_scanned" ||
289
+ input.searchedNegativeSet.untriedBroadenings > 0 ||
290
+ productiveYield) {
291
+ return {
292
+ exhausted: false,
293
+ falsifyingClause: "source_frontier_scan_terminated",
294
+ };
295
+ }
296
+ return { exhausted: true };
297
+ },
298
+ };
299
+ /** Future modules that must IMPORT the predicate rather than restate it. */
300
+ export const REFILL_V3_EXHAUSTION_PREDICATE_CONSUMERS = [
301
+ "mcp/sellable/src/tools/refill-v3-advance-contract.ts",
302
+ "mcp/sellable/src/tools/refill-v3-edit-selected-campaign-contract.ts",
303
+ ];
304
+ // ───────────────────────────────────────────────────────────────────────────
305
+ // Closed AI action and typed traversal-result vocabularies
306
+ // ───────────────────────────────────────────────────────────────────────────
307
+ /**
308
+ * The ONE closed AI action vocabulary. Wave 7 validates model output against
309
+ * this constant and GENERATES its prompt's allowed-action list from it; wave 8
310
+ * consumes it. Neither may declare a second list (error law 1).
311
+ */
312
+ export const REFILL_V3_CAMPAIGN_ATTENTION_ACTIONS = [
313
+ "rerun_errored_cells",
314
+ "repair_campaign_config",
315
+ "enrich_bounded_sample",
316
+ "regenerate_stale_messages",
317
+ "patch_filter_criteria",
318
+ "patch_source_criteria",
319
+ "broaden_source_family",
320
+ "request_rubric_trial",
321
+ "await_active_job",
322
+ "record_source_frontier_exhausted",
323
+ ];
324
+ /**
325
+ * Typed traversal RESULTS. `source_frontier_exhausted` and
326
+ * `health_recheck_required` live here and are never unreceipted AI actions:
327
+ * the AI records exhaustion through `record_source_frontier_exhausted`, and the
328
+ * deterministic authority answers with the result.
329
+ */
330
+ export const REFILL_V3_CAMPAIGN_ATTENTION_RESULTS = [
331
+ "cells_rerun",
332
+ "campaign_config_repaired",
333
+ "bounded_sample_enriched",
334
+ "stale_messages_regenerated",
335
+ "filter_criteria_patched",
336
+ "source_criteria_patched",
337
+ "source_family_broadened",
338
+ "rubric_trial_requested",
339
+ "active_job_awaited",
340
+ "source_frontier_exhausted",
341
+ "health_recheck_required",
342
+ "no_action_available",
343
+ ];
344
+ /**
345
+ * Prompt assets that must GENERATE their allowed outputs from the constants
346
+ * above rather than hand-maintain a second list. Every named asset that exists
347
+ * is bound by the shared-vocabulary assertion; the assets themselves are
348
+ * authored by their owning wave-7 plan.
349
+ */
350
+ export const REFILL_V3_ATTENTION_ASSET_CONTRACT = [
351
+ {
352
+ asset: "mcp/sellable/skills/refill-sends-work-campaign/SKILL.md",
353
+ generatedFrom: "REFILL_V3_CAMPAIGN_ATTENTION_ACTIONS",
354
+ ownerPlan: "146.5-09",
355
+ },
356
+ {
357
+ asset: "mcp/sellable/skills/refill-sends-waterfall/SKILL.md",
358
+ generatedFrom: "REFILL_V3_WATERFALL_PROPOSAL_SCHEMA",
359
+ ownerPlan: "146.5-09",
360
+ },
361
+ ];
362
+ /**
363
+ * Prose that must never survive in a generated asset. Deterministic ladder
364
+ * step 3 owns the hand-off to `refill_v3_fill_ready`; an AI action token for it
365
+ * would put a deterministic transition inside the model's vocabulary.
366
+ */
367
+ export const REFILL_V3_ATTENTION_FORBIDDEN_ASSET_INSTRUCTIONS = [
368
+ "refill_v3_fill_ready",
369
+ "approve_and_hand_off",
370
+ "hand off to refill_v3_fill_ready",
371
+ ];
372
+ // ───────────────────────────────────────────────────────────────────────────
373
+ // The packet
374
+ // ───────────────────────────────────────────────────────────────────────────
375
+ export const REFILL_V3_CAMPAIGN_ATTENTION_SCHEMA_NAMESPACE = "refill_v3_campaign_attention";
376
+ export const REFILL_V3_CAMPAIGN_ATTENTION_SCHEMA_MAJOR = 1;
377
+ export const REFILL_V3_CAMPAIGN_ATTENTION_SCHEMA_VERSION = "refill_v3_campaign_attention.v1.0";
378
+ export const REFILL_V3_CAMPAIGN_ATTENTION_LANE_ACTION_TYPES = Object.freeze({
379
+ connection_invite: "send_invite",
380
+ paid_inmail: "send_inmail_closed",
381
+ });
382
+ /**
383
+ * Identity, payload, policy, and bookkeeping classes the packet must never
384
+ * carry. A raw row/cell/provider payload would move the table into the model;
385
+ * a rank/selection field would move backend authority into the model; a retry
386
+ * counter or cursor would recreate the durable state machine this phase deletes.
387
+ */
388
+ export const REFILL_V3_ATTENTION_FORBIDDEN_KEYS = [
389
+ "rows",
390
+ "cells",
391
+ "rowIds",
392
+ "cellIds",
393
+ "rowPayload",
394
+ "cellPayload",
395
+ "providerResponse",
396
+ "providerPayload",
397
+ "rank",
398
+ "ranking",
399
+ "graph",
400
+ "selectedCampaign",
401
+ "waterfall",
402
+ "retryCount",
403
+ "attempts",
404
+ "cursor",
405
+ "continuation",
406
+ "prompt",
407
+ "instructions",
408
+ "systemPrompt",
409
+ ];
410
+ export const REFILL_V3_CAMPAIGN_ATTENTION_PACKET_SCHEMA = Object.freeze({
411
+ type: "object",
412
+ additionalProperties: false,
413
+ required: ["scope", "evidence"],
414
+ properties: {
415
+ scope: {
416
+ type: "object",
417
+ additionalProperties: false,
418
+ required: [
419
+ "workspaceId",
420
+ "campaignId",
421
+ "tableId",
422
+ "campaignVersion",
423
+ "evidenceFingerprint",
424
+ "senderId",
425
+ "date",
426
+ "lane",
427
+ "actionType",
428
+ ],
429
+ properties: {
430
+ workspaceId: { type: "string", minLength: 1 },
431
+ campaignId: { type: "string", minLength: 1 },
432
+ tableId: { type: "string", minLength: 1 },
433
+ campaignVersion: { type: "integer", minimum: 0 },
434
+ evidenceFingerprint: { type: "string", minLength: 1 },
435
+ senderId: { type: "string", minLength: 1 },
436
+ date: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" },
437
+ lane: {
438
+ type: "string",
439
+ enum: Object.keys(REFILL_V3_CAMPAIGN_ATTENTION_LANE_ACTION_TYPES),
440
+ },
441
+ actionType: {
442
+ type: "string",
443
+ enum: Object.values(REFILL_V3_CAMPAIGN_ATTENTION_LANE_ACTION_TYPES),
444
+ },
445
+ },
446
+ },
447
+ evidence: {
448
+ type: "object",
449
+ additionalProperties: false,
450
+ required: [
451
+ "campaignType",
452
+ "campaignObjective",
453
+ "protectedFitDigest",
454
+ "leadSourceFamily",
455
+ "provider",
456
+ "sourceLineage",
457
+ "priorYieldReceipt",
458
+ "searchedNegativeSetFingerprints",
459
+ "remainingGap",
460
+ "supply",
461
+ "activeJob",
462
+ "failedJobRef",
463
+ "diagnosticRowRef",
464
+ ],
465
+ properties: {
466
+ campaignType: { type: "string" },
467
+ campaignObjective: { type: "string" },
468
+ protectedFitDigest: { type: "string" },
469
+ leadSourceFamily: {
470
+ type: "string",
471
+ enum: [...REFILL_V3_LEAD_SOURCE_FAMILIES],
472
+ },
473
+ provider: { type: ["string", "null"] },
474
+ selectedLeadListId: { type: ["string", "null"] },
475
+ providerSearchReference: {
476
+ type: ["object", "null"],
477
+ additionalProperties: false,
478
+ required: [
479
+ "workspaceId",
480
+ "provider",
481
+ "sourceTableId",
482
+ "searchId",
483
+ "resolutionOrigin",
484
+ "owner",
485
+ "basisFingerprint",
486
+ ],
487
+ properties: {
488
+ workspaceId: { type: "string" },
489
+ provider: { type: "string", enum: ["sales-nav", "prospeo"] },
490
+ sourceTableId: { type: "string" },
491
+ searchId: { type: "string" },
492
+ resolutionOrigin: {
493
+ type: "string",
494
+ enum: ["config_import_job", "exact_table_job", "find_leads_run"],
495
+ },
496
+ owner: {
497
+ type: "object",
498
+ additionalProperties: false,
499
+ required: ["kind", "id"],
500
+ properties: {
501
+ kind: {
502
+ type: "string",
503
+ enum: ["campaign", "find_leads_run"],
504
+ },
505
+ id: { type: "string" },
506
+ },
507
+ },
508
+ basisFingerprint: { type: "string" },
509
+ },
510
+ },
511
+ sourceLineage: { type: "array", items: { type: "string" } },
512
+ priorYieldReceipt: {
513
+ type: ["object", "null"],
514
+ additionalProperties: false,
515
+ required: ["family", "evaluated", "passes"],
516
+ properties: {
517
+ family: {
518
+ type: "string",
519
+ enum: [...REFILL_V3_LEAD_SOURCE_FAMILIES],
520
+ },
521
+ evaluated: { type: "integer", minimum: 0 },
522
+ passes: { type: "integer", minimum: 0 },
523
+ },
524
+ },
525
+ searchedNegativeSetFingerprints: {
526
+ type: "array",
527
+ items: { type: "string" },
528
+ },
529
+ remainingGap: { type: "integer", minimum: 0 },
530
+ supply: {
531
+ type: "object",
532
+ additionalProperties: false,
533
+ required: [...REFILL_V3_SUPPLY_BUCKETS],
534
+ properties: {
535
+ readyToSchedule: { type: "integer", minimum: 0 },
536
+ readyForApproval: { type: "integer", minimum: 0 },
537
+ readyForMessageGeneration: { type: "integer", minimum: 0 },
538
+ needsEnrichment: { type: "integer", minimum: 0 },
539
+ },
540
+ },
541
+ activeJob: {
542
+ type: ["object", "null"],
543
+ additionalProperties: false,
544
+ required: ["id", "kind", "status"],
545
+ properties: {
546
+ id: { type: "string", minLength: 1 },
547
+ kind: { type: "string", enum: [...REFILL_V3_ACTIVE_JOB_KINDS] },
548
+ status: {
549
+ type: "string",
550
+ enum: [...REFILL_V3_ACTIVE_JOB_STATUSES],
551
+ },
552
+ },
553
+ },
554
+ failedJobRef: { type: ["string", "null"] },
555
+ diagnosticRowRef: {
556
+ type: ["object", "null"],
557
+ additionalProperties: false,
558
+ required: ["count", "selectorRef"],
559
+ properties: {
560
+ count: {
561
+ type: "integer",
562
+ minimum: 0,
563
+ maximum: REFILL_V3_DIAGNOSTIC_SAMPLE_CAP,
564
+ },
565
+ selectorRef: { type: "string", minLength: 1 },
566
+ },
567
+ },
568
+ // Additive enrichment (146.5-12 §84): OPTIONAL, evidence stays closed.
569
+ repairableExistingRows: { type: "integer", minimum: 0 },
570
+ retryExhaustedRows: { type: "integer", minimum: 0 },
571
+ laneSchedulable: { type: ["integer", "null"], minimum: 0 },
572
+ },
573
+ },
574
+ },
575
+ });
576
+ // ───────────────────────────────────────────────────────────────────────────
577
+ // Decoding
578
+ // ───────────────────────────────────────────────────────────────────────────
579
+ function isRecord(value) {
580
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
581
+ }
582
+ function isNonEmptyString(value) {
583
+ return typeof value === "string" && value.trim().length > 0;
584
+ }
585
+ function isWholeNumber(value, max) {
586
+ return (Number.isInteger(value) &&
587
+ value >= 0 &&
588
+ (max === undefined || value <= max));
589
+ }
590
+ function isOneOf(value, options) {
591
+ return (typeof value === "string" && options.includes(value));
592
+ }
593
+ function isStringArray(value) {
594
+ return (Array.isArray(value) && value.every((item) => typeof item === "string"));
595
+ }
596
+ function isPacketDate(value) {
597
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
598
+ return false;
599
+ }
600
+ const parsed = new Date(`${value}T00:00:00.000Z`);
601
+ return (!Number.isNaN(parsed.getTime()) &&
602
+ parsed.toISOString().slice(0, 10) === value);
603
+ }
604
+ /**
605
+ * Tolerance is ONE-WAY. A harmless additive field at a recognized level is
606
+ * stripped, but a known policy/identity leak key is fatal: a decoder that
607
+ * quietly forwards a raw row payload or a rank field is how backend authority
608
+ * silently moves into the model.
609
+ */
610
+ function leaksForbiddenKey(record) {
611
+ return REFILL_V3_ATTENTION_FORBIDDEN_KEYS.some((key) => key in record);
612
+ }
613
+ function decodeScope(value) {
614
+ if (!isRecord(value) || leaksForbiddenKey(value))
615
+ return null;
616
+ const lane = value.lane;
617
+ if (!isOneOf(lane, Object.keys(REFILL_V3_CAMPAIGN_ATTENTION_LANE_ACTION_TYPES)) ||
618
+ value.actionType !== REFILL_V3_CAMPAIGN_ATTENTION_LANE_ACTION_TYPES[lane]) {
619
+ return null;
620
+ }
621
+ if (!isNonEmptyString(value.workspaceId) ||
622
+ !isNonEmptyString(value.campaignId) ||
623
+ !isNonEmptyString(value.tableId) ||
624
+ !isWholeNumber(value.campaignVersion) ||
625
+ !isNonEmptyString(value.evidenceFingerprint) ||
626
+ !isNonEmptyString(value.senderId) ||
627
+ !isPacketDate(value.date)) {
628
+ return null;
629
+ }
630
+ return {
631
+ workspaceId: value.workspaceId,
632
+ campaignId: value.campaignId,
633
+ tableId: value.tableId,
634
+ campaignVersion: value.campaignVersion,
635
+ evidenceFingerprint: value.evidenceFingerprint,
636
+ senderId: value.senderId,
637
+ date: value.date,
638
+ lane,
639
+ actionType: REFILL_V3_CAMPAIGN_ATTENTION_LANE_ACTION_TYPES[lane],
640
+ };
641
+ }
642
+ function decodeSupply(value) {
643
+ if (!isRecord(value) || leaksForbiddenKey(value))
644
+ return null;
645
+ for (const bucket of REFILL_V3_SUPPLY_BUCKETS) {
646
+ if (!isWholeNumber(value[bucket]))
647
+ return null;
648
+ }
649
+ return {
650
+ readyToSchedule: value.readyToSchedule,
651
+ readyForApproval: value.readyForApproval,
652
+ readyForMessageGeneration: value.readyForMessageGeneration,
653
+ needsEnrichment: value.needsEnrichment,
654
+ };
655
+ }
656
+ function decodeActiveJob(value) {
657
+ if (value === null)
658
+ return { ok: true, value: null };
659
+ if (!isRecord(value) || leaksForbiddenKey(value))
660
+ return { ok: false };
661
+ if (!isNonEmptyString(value.id) ||
662
+ !isOneOf(value.kind, REFILL_V3_ACTIVE_JOB_KINDS) ||
663
+ !isOneOf(value.status, REFILL_V3_ACTIVE_JOB_STATUSES)) {
664
+ return { ok: false };
665
+ }
666
+ return {
667
+ ok: true,
668
+ value: { id: value.id, kind: value.kind, status: value.status },
669
+ };
670
+ }
671
+ function decodeYieldReceipt(value) {
672
+ if (value === null)
673
+ return { ok: true, value: null };
674
+ if (!isRecord(value) || leaksForbiddenKey(value))
675
+ return { ok: false };
676
+ if (!isOneOf(value.family, REFILL_V3_LEAD_SOURCE_FAMILIES) ||
677
+ !isWholeNumber(value.evaluated) ||
678
+ !isWholeNumber(value.passes)) {
679
+ return { ok: false };
680
+ }
681
+ return {
682
+ ok: true,
683
+ value: {
684
+ family: value.family,
685
+ evaluated: value.evaluated,
686
+ passes: value.passes,
687
+ },
688
+ };
689
+ }
690
+ function decodeDiagnosticRef(value) {
691
+ if (value === null)
692
+ return { ok: true, value: null };
693
+ if (!isRecord(value) || leaksForbiddenKey(value))
694
+ return { ok: false };
695
+ if (!isWholeNumber(value.count, REFILL_V3_DIAGNOSTIC_SAMPLE_CAP) ||
696
+ !isNonEmptyString(value.selectorRef)) {
697
+ return { ok: false };
698
+ }
699
+ return {
700
+ ok: true,
701
+ value: { count: value.count, selectorRef: value.selectorRef },
702
+ };
703
+ }
704
+ function decodeProviderSearchReference(value) {
705
+ if (value === null)
706
+ return { ok: true, value: null };
707
+ if (!isRecord(value) || leaksForbiddenKey(value))
708
+ return { ok: false };
709
+ const owner = value.owner;
710
+ if (!isRecord(owner) || leaksForbiddenKey(owner))
711
+ return { ok: false };
712
+ if (!isNonEmptyString(value.workspaceId) ||
713
+ !isOneOf(value.provider, ["sales-nav", "prospeo"]) ||
714
+ !isNonEmptyString(value.sourceTableId) ||
715
+ !isNonEmptyString(value.searchId) ||
716
+ !isOneOf(value.resolutionOrigin, [
717
+ "config_import_job",
718
+ "exact_table_job",
719
+ "find_leads_run",
720
+ ]) ||
721
+ !isOneOf(owner.kind, ["campaign", "find_leads_run"]) ||
722
+ !isNonEmptyString(owner.id) ||
723
+ !isNonEmptyString(value.basisFingerprint)) {
724
+ return { ok: false };
725
+ }
726
+ return {
727
+ ok: true,
728
+ value: {
729
+ workspaceId: value.workspaceId,
730
+ provider: value.provider,
731
+ sourceTableId: value.sourceTableId,
732
+ searchId: value.searchId,
733
+ resolutionOrigin: value.resolutionOrigin,
734
+ owner: { kind: owner.kind, id: owner.id },
735
+ basisFingerprint: value.basisFingerprint,
736
+ },
737
+ };
738
+ }
739
+ function decodeEvidence(value) {
740
+ if (!isRecord(value) || leaksForbiddenKey(value))
741
+ return null;
742
+ if (typeof value.campaignType !== "string" ||
743
+ typeof value.campaignObjective !== "string" ||
744
+ typeof value.protectedFitDigest !== "string" ||
745
+ !isOneOf(value.leadSourceFamily, REFILL_V3_LEAD_SOURCE_FAMILIES) ||
746
+ !(value.provider === null || typeof value.provider === "string") ||
747
+ !isStringArray(value.sourceLineage) ||
748
+ !isStringArray(value.searchedNegativeSetFingerprints) ||
749
+ !isWholeNumber(value.remainingGap) ||
750
+ !(value.failedJobRef === null || typeof value.failedJobRef === "string")) {
751
+ return null;
752
+ }
753
+ if (!("priorYieldReceipt" in value) ||
754
+ !("activeJob" in value) ||
755
+ !("failedJobRef" in value) ||
756
+ !("diagnosticRowRef" in value)) {
757
+ return null;
758
+ }
759
+ const supply = decodeSupply(value.supply);
760
+ if (supply === null)
761
+ return null;
762
+ const activeJob = decodeActiveJob(value.activeJob);
763
+ if (!activeJob.ok)
764
+ return null;
765
+ const priorYieldReceipt = decodeYieldReceipt(value.priorYieldReceipt);
766
+ if (!priorYieldReceipt.ok)
767
+ return null;
768
+ const diagnosticRowRef = decodeDiagnosticRef(value.diagnosticRowRef);
769
+ if (!diagnosticRowRef.ok)
770
+ return null;
771
+ // Additive enrichment (146.5-12 §84): absent stays absent; a malformed
772
+ // present value rejects the packet rather than laundering into a number.
773
+ const repairableExistingRows = "repairableExistingRows" in value
774
+ ? value.repairableExistingRows
775
+ : undefined;
776
+ if (repairableExistingRows !== undefined &&
777
+ !isWholeNumber(repairableExistingRows)) {
778
+ return null;
779
+ }
780
+ const retryExhaustedRows = "retryExhaustedRows" in value ? value.retryExhaustedRows : undefined;
781
+ if (retryExhaustedRows !== undefined && !isWholeNumber(retryExhaustedRows)) {
782
+ return null;
783
+ }
784
+ const laneSchedulable = "laneSchedulable" in value ? value.laneSchedulable : undefined;
785
+ if (laneSchedulable !== undefined &&
786
+ laneSchedulable !== null &&
787
+ !isWholeNumber(laneSchedulable)) {
788
+ return null;
789
+ }
790
+ const selectedLeadListId = "selectedLeadListId" in value ? value.selectedLeadListId : undefined;
791
+ if (selectedLeadListId !== undefined &&
792
+ selectedLeadListId !== null &&
793
+ !isNonEmptyString(selectedLeadListId)) {
794
+ return null;
795
+ }
796
+ const providerSearchReference = "providerSearchReference" in value
797
+ ? decodeProviderSearchReference(value.providerSearchReference)
798
+ : undefined;
799
+ if (providerSearchReference && !providerSearchReference.ok)
800
+ return null;
801
+ return {
802
+ campaignType: value.campaignType,
803
+ campaignObjective: value.campaignObjective,
804
+ protectedFitDigest: value.protectedFitDigest,
805
+ leadSourceFamily: value.leadSourceFamily,
806
+ provider: value.provider,
807
+ ...(selectedLeadListId === undefined ? {} : { selectedLeadListId }),
808
+ ...(providerSearchReference === undefined
809
+ ? {}
810
+ : { providerSearchReference: providerSearchReference.value }),
811
+ sourceLineage: [...value.sourceLineage],
812
+ priorYieldReceipt: priorYieldReceipt.value,
813
+ searchedNegativeSetFingerprints: [...value.searchedNegativeSetFingerprints],
814
+ remainingGap: value.remainingGap,
815
+ supply,
816
+ activeJob: activeJob.value,
817
+ failedJobRef: value.failedJobRef,
818
+ diagnosticRowRef: diagnosticRowRef.value,
819
+ ...(repairableExistingRows === undefined ? {} : { repairableExistingRows }),
820
+ ...(retryExhaustedRows === undefined ? {} : { retryExhaustedRows }),
821
+ ...(laneSchedulable === undefined ? {} : { laneSchedulable }),
822
+ };
823
+ }
824
+ /**
825
+ * Sole unknown-value decoder for the packet.
826
+ *
827
+ * Strict-in, tolerant-out, lossless. `null` means "not this contract"; the
828
+ * typed unsupported VALUE means "a major this build cannot speak", which is a
829
+ * different fact and must never be collapsed into `null` or business zero.
830
+ */
831
+ export function decodeRefillV3CampaignAttentionPacket(value) {
832
+ if (!isRecord(value))
833
+ return null;
834
+ const support = resolveRefillV3SchemaVersion(value.schemaVersion, REFILL_V3_CAMPAIGN_ATTENTION_SCHEMA_NAMESPACE, REFILL_V3_CAMPAIGN_ATTENTION_SCHEMA_MAJOR);
835
+ if (!support.supported) {
836
+ return support.reason === REFILL_V3_SCHEMA_VERSION_UNSUPPORTED
837
+ ? REFILL_V3_SCHEMA_VERSION_UNSUPPORTED
838
+ : null;
839
+ }
840
+ if (leaksForbiddenKey(value))
841
+ return null;
842
+ const scope = decodeScope(value.scope);
843
+ if (scope === null)
844
+ return null;
845
+ const evidence = decodeEvidence(value.evidence);
846
+ if (evidence === null)
847
+ return null;
848
+ return { scope, evidence };
849
+ }
850
+ /**
851
+ * Wire form of a decoded packet. `schemaVersion` is TRANSPORT, not business, so
852
+ * it is gated and stripped on decode and re-attached here. Every locked field
853
+ * is projected explicitly, because a silently dropped property is exactly the
854
+ * defect that turns a round trip into a confident wrong success.
855
+ */
856
+ export function encodeRefillV3CampaignAttentionPacket(packet) {
857
+ return {
858
+ schemaVersion: REFILL_V3_CAMPAIGN_ATTENTION_SCHEMA_VERSION,
859
+ scope: {
860
+ workspaceId: packet.scope.workspaceId,
861
+ campaignId: packet.scope.campaignId,
862
+ tableId: packet.scope.tableId,
863
+ campaignVersion: packet.scope.campaignVersion,
864
+ evidenceFingerprint: packet.scope.evidenceFingerprint,
865
+ senderId: packet.scope.senderId,
866
+ date: packet.scope.date,
867
+ lane: packet.scope.lane,
868
+ actionType: packet.scope.actionType,
869
+ },
870
+ evidence: {
871
+ campaignType: packet.evidence.campaignType,
872
+ campaignObjective: packet.evidence.campaignObjective,
873
+ protectedFitDigest: packet.evidence.protectedFitDigest,
874
+ leadSourceFamily: packet.evidence.leadSourceFamily,
875
+ provider: packet.evidence.provider,
876
+ ...(packet.evidence.selectedLeadListId === undefined
877
+ ? {}
878
+ : { selectedLeadListId: packet.evidence.selectedLeadListId }),
879
+ ...(packet.evidence.providerSearchReference === undefined
880
+ ? {}
881
+ : {
882
+ providerSearchReference: packet.evidence.providerSearchReference
883
+ ? {
884
+ ...packet.evidence.providerSearchReference,
885
+ owner: { ...packet.evidence.providerSearchReference.owner },
886
+ }
887
+ : null,
888
+ }),
889
+ sourceLineage: [...packet.evidence.sourceLineage],
890
+ priorYieldReceipt: packet.evidence.priorYieldReceipt,
891
+ searchedNegativeSetFingerprints: [
892
+ ...packet.evidence.searchedNegativeSetFingerprints,
893
+ ],
894
+ remainingGap: packet.evidence.remainingGap,
895
+ supply: { ...packet.evidence.supply },
896
+ activeJob: packet.evidence.activeJob,
897
+ failedJobRef: packet.evidence.failedJobRef,
898
+ diagnosticRowRef: packet.evidence.diagnosticRowRef,
899
+ // Additive enrichment (146.5-12 §84): projected explicitly when present.
900
+ ...(packet.evidence.repairableExistingRows === undefined
901
+ ? {}
902
+ : { repairableExistingRows: packet.evidence.repairableExistingRows }),
903
+ ...(packet.evidence.retryExhaustedRows === undefined
904
+ ? {}
905
+ : { retryExhaustedRows: packet.evidence.retryExhaustedRows }),
906
+ ...(packet.evidence.laneSchedulable === undefined
907
+ ? {}
908
+ : { laneSchedulable: packet.evidence.laneSchedulable }),
909
+ },
910
+ };
911
+ }