@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,920 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refill V3 `advance` PACKAGE seam (Phase 146.5 Plan 10, RV3FLOW-001).
|
|
3
|
+
*
|
|
4
|
+
* This module owns the PUBLIC vocabulary of the one-step coordinator:
|
|
5
|
+
*
|
|
6
|
+
* 1. the closed `{workspaceId,scope}` declarative input schema;
|
|
7
|
+
* 2. the closed five-variant result union plus its top-level output schema; and
|
|
8
|
+
* 3. the SOLE unknown-value result decoder every installed client runs.
|
|
9
|
+
*
|
|
10
|
+
* It declares NO packet shape and NO second AI action list: the
|
|
11
|
+
* `campaign_attention_required` packet, its decoder, its schema, and the closed
|
|
12
|
+
* action vocabulary are owned by Plan 02 T3 in
|
|
13
|
+
* `refill-v3-attention-packet-contract.ts` and only RE-EXPORTED through this
|
|
14
|
+
* seam (assumption `A10-PACKET-CONTRACT-IMPORTED`). The five-conjunct exhaustion
|
|
15
|
+
* predicate is likewise imported, never restated.
|
|
16
|
+
*
|
|
17
|
+
* Constraints inherited from the sibling contracts: zero imports outside this
|
|
18
|
+
* package, no app `src/**`, hand-rolled guards, as-const JSON-Schema literals,
|
|
19
|
+
* strict-in/tolerant-out decoding, and a compatible-RANGE schema-version gate.
|
|
20
|
+
*/
|
|
21
|
+
import { REFILL_V3_CAMPAIGN_ATTENTION_ACTIONS, REFILL_V3_CAMPAIGN_ATTENTION_PACKET_SCHEMA, REFILL_V3_CAMPAIGN_ATTENTION_RESULTS, REFILL_V3_EXHAUSTION_PREDICATE, REFILL_V3_LEAD_SOURCE_FAMILIES, REFILL_V3_SCHEMA_VERSION_UNSUPPORTED, REFILL_V3_SUPPLY_BUCKETS, decodeRefillV3CampaignAttentionPacket, encodeRefillV3CampaignAttentionPacket, resolveRefillV3SchemaVersion, } from "./refill-v3-attention-packet-contract.js";
|
|
22
|
+
import { REFILL_V3_FILL_READY_REASON_CODES } from "./refill-v3-fill-ready-contract.js";
|
|
23
|
+
import { REFILL_V3_WORLD_STATE_LANES, } from "./refill-v3-world-state-contract.js";
|
|
24
|
+
/**
|
|
25
|
+
* The imported seams, re-exported so a consumer needs exactly ONE import to
|
|
26
|
+
* produce a packet, validate an AI action, or evaluate exhaustion. These are
|
|
27
|
+
* VALUE re-exports of a single declaration, never a second declaration.
|
|
28
|
+
*/
|
|
29
|
+
export { REFILL_V3_CAMPAIGN_ATTENTION_ACTIONS, REFILL_V3_CAMPAIGN_ATTENTION_RESULTS, REFILL_V3_EXHAUSTION_PREDICATE, decodeRefillV3CampaignAttentionPacket, encodeRefillV3CampaignAttentionPacket, };
|
|
30
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
31
|
+
// Schema identity
|
|
32
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
33
|
+
export const REFILL_V3_ADVANCE_SCHEMA_NAMESPACE = "refill_v3_advance";
|
|
34
|
+
export const REFILL_V3_ADVANCE_SCHEMA_MAJOR = 1;
|
|
35
|
+
export const REFILL_V3_ADVANCE_SCHEMA_VERSION = "refill_v3_advance.v1.0";
|
|
36
|
+
export function resolveRefillV3AdvanceSchemaVersion(value) {
|
|
37
|
+
return resolveRefillV3SchemaVersion(value, REFILL_V3_ADVANCE_SCHEMA_NAMESPACE, REFILL_V3_ADVANCE_SCHEMA_MAJOR);
|
|
38
|
+
}
|
|
39
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
40
|
+
// Closed request vocabulary
|
|
41
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
42
|
+
/**
|
|
43
|
+
* The ONE public key set.
|
|
44
|
+
*
|
|
45
|
+
* `yolo` is the OPERATOR'S AUTONOMY GRANT, and it is the only optional key. It
|
|
46
|
+
* is deliberately NOT a mode: it carries no campaign selection, no execution
|
|
47
|
+
* variant, and no enum. Evergreen-ness stays DERIVED from the workspace's own
|
|
48
|
+
* campaigns, so a caller still cannot ask for a particular campaign to start —
|
|
49
|
+
* it can only grant, or withhold, the autonomy to start one at all.
|
|
50
|
+
*
|
|
51
|
+
* The linkage is V1's, restored rather than invented: `flow.v1.json`'s
|
|
52
|
+
* `yoloMode` block carries `approvalSource: "explicit_yolo_flag"` and lists
|
|
53
|
+
* `start_paused_campaign` among its `autoExecutableActions`, and `refill_sends`
|
|
54
|
+
* states it as "In --yolo or after one Accept ... a fresh exact
|
|
55
|
+
* start_paused_campaign packet authorizes starting only that selected PAUSED
|
|
56
|
+
* fallback". The V3 cutover dropped that grant; this restores it.
|
|
57
|
+
*/
|
|
58
|
+
export const REFILL_V3_ADVANCE_INPUT_KEYS = [
|
|
59
|
+
"workspaceId",
|
|
60
|
+
"scope",
|
|
61
|
+
"yolo",
|
|
62
|
+
];
|
|
63
|
+
/** The keys a caller MUST supply. `yolo` is optional and defaults to false. */
|
|
64
|
+
export const REFILL_V3_ADVANCE_REQUIRED_INPUT_KEYS = [
|
|
65
|
+
"workspaceId",
|
|
66
|
+
"scope",
|
|
67
|
+
];
|
|
68
|
+
/**
|
|
69
|
+
* Keys that recreate the durable state machine this phase deletes. They are
|
|
70
|
+
* enumerated so a REFUSAL can name the exact offending field instead of a
|
|
71
|
+
* generic "unknown key", and so a source scan can prove no caller sends them.
|
|
72
|
+
*/
|
|
73
|
+
export const REFILL_V3_ADVANCE_FORBIDDEN_INPUT_KEYS = [
|
|
74
|
+
"continuation",
|
|
75
|
+
"taskReceipts",
|
|
76
|
+
"authorityReceiptFacts",
|
|
77
|
+
"receiptFacts",
|
|
78
|
+
"retryCount",
|
|
79
|
+
"attempt",
|
|
80
|
+
"attempts",
|
|
81
|
+
"cursor",
|
|
82
|
+
"rows",
|
|
83
|
+
"row",
|
|
84
|
+
"actions",
|
|
85
|
+
"action",
|
|
86
|
+
"progress",
|
|
87
|
+
"runId",
|
|
88
|
+
"ledger",
|
|
89
|
+
"waterfall",
|
|
90
|
+
"evaluatedAt",
|
|
91
|
+
];
|
|
92
|
+
const ADVANCE_SCOPE_SCHEMA = {
|
|
93
|
+
oneOf: [
|
|
94
|
+
{
|
|
95
|
+
type: "object",
|
|
96
|
+
additionalProperties: false,
|
|
97
|
+
required: ["kind", "days"],
|
|
98
|
+
properties: {
|
|
99
|
+
kind: { type: "string", const: "sender_local_horizon" },
|
|
100
|
+
days: { type: "integer", minimum: 1, maximum: 3 },
|
|
101
|
+
senderIds: {
|
|
102
|
+
type: "array",
|
|
103
|
+
items: { type: "string", minLength: 1 },
|
|
104
|
+
minItems: 1,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
type: "object",
|
|
110
|
+
additionalProperties: false,
|
|
111
|
+
required: ["kind", "senderDates"],
|
|
112
|
+
properties: {
|
|
113
|
+
kind: { type: "string", const: "exact_sender_dates" },
|
|
114
|
+
senderDates: {
|
|
115
|
+
type: "array",
|
|
116
|
+
minItems: 1,
|
|
117
|
+
items: {
|
|
118
|
+
type: "object",
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
required: ["senderId", "dates"],
|
|
121
|
+
properties: {
|
|
122
|
+
senderId: { type: "string", minLength: 1 },
|
|
123
|
+
dates: {
|
|
124
|
+
type: "array",
|
|
125
|
+
minItems: 1,
|
|
126
|
+
items: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
};
|
|
135
|
+
export const REFILL_V3_ADVANCE_INPUT_SCHEMA = Object.freeze({
|
|
136
|
+
type: "object",
|
|
137
|
+
additionalProperties: false,
|
|
138
|
+
required: [...REFILL_V3_ADVANCE_REQUIRED_INPUT_KEYS],
|
|
139
|
+
properties: {
|
|
140
|
+
workspaceId: {
|
|
141
|
+
type: "string",
|
|
142
|
+
minLength: 1,
|
|
143
|
+
description: "Explicit request-scoped workspace id. Pass this instead of switching the shared active workspace.",
|
|
144
|
+
},
|
|
145
|
+
scope: {
|
|
146
|
+
...ADVANCE_SCOPE_SCHEMA,
|
|
147
|
+
description: "Either a 1-3 day sender-local horizon or an exact sender/date set. There is no lane input, no row input, and no mode input.",
|
|
148
|
+
},
|
|
149
|
+
yolo: {
|
|
150
|
+
type: "boolean",
|
|
151
|
+
description: "The operator's AUTONOMY GRANT, true only when the operator actually asked for --yolo. It is not a mode and selects nothing: WHICH campaign may start is derived from the workspace's own campaigns. It authorizes exactly one thing this call could not otherwise do — starting one already-eligible PAUSED derived-evergreen campaign once every active campaign for a row is exhausted. Omit it, or pass false, for a conservative run that reports that exhaustion instead of acting on it.",
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
156
|
+
// Closed result vocabulary
|
|
157
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
158
|
+
export const REFILL_V3_ADVANCE_RESULT_KINDS = [
|
|
159
|
+
"advanced",
|
|
160
|
+
"campaign_attention_required",
|
|
161
|
+
"awaiting_external_change",
|
|
162
|
+
"complete",
|
|
163
|
+
"blocked",
|
|
164
|
+
];
|
|
165
|
+
/**
|
|
166
|
+
* The closed set of transitions the coordinator may execute. Each names the ONE
|
|
167
|
+
* existing authority that owns it; the coordinator never invents an authority
|
|
168
|
+
* and never carries executable prose.
|
|
169
|
+
*/
|
|
170
|
+
export const REFILL_V3_ADVANCE_ACTION_KINDS = [
|
|
171
|
+
"refresh_paid_credit_fact",
|
|
172
|
+
"recheck_sender_connection",
|
|
173
|
+
"fill_ready",
|
|
174
|
+
"start_evergreen_campaign",
|
|
175
|
+
];
|
|
176
|
+
/** The exact existing owner each action key dispatches to. */
|
|
177
|
+
export const REFILL_V3_ADVANCE_ACTION_AUTHORITIES = Object.freeze({
|
|
178
|
+
refresh_paid_credit_fact: "getOrRefreshPaidInmailCreditFact",
|
|
179
|
+
recheck_sender_connection: "recheckRefillV3SenderConnection",
|
|
180
|
+
fill_ready: "fillRefillV3Ready",
|
|
181
|
+
start_evergreen_campaign: "startRefillV3EvergreenCampaign",
|
|
182
|
+
});
|
|
183
|
+
/**
|
|
184
|
+
* The TOTAL action precedence. Two locked requirements otherwise appear to
|
|
185
|
+
* conflict: a required exact sender/lane refresh (111o hand-back) must preempt
|
|
186
|
+
* an otherwise-eligible ready action on the same paid row, and health must be
|
|
187
|
+
* resolved before the AI campaign seam is reachable at all.
|
|
188
|
+
*/
|
|
189
|
+
export const REFILL_V3_ADVANCE_ACTION_PRECEDENCE = [
|
|
190
|
+
"refresh_paid_credit_fact",
|
|
191
|
+
"recheck_sender_connection",
|
|
192
|
+
"fill_ready",
|
|
193
|
+
"campaign_attention",
|
|
194
|
+
];
|
|
195
|
+
/** Closed terminal codes. `complete` carries exactly one of these. */
|
|
196
|
+
export const REFILL_V3_ADVANCE_TERMINAL_CODES = [
|
|
197
|
+
/** Every eligible campaign in every traversed row is receipt-proven spent. */
|
|
198
|
+
"active_campaigns_exhausted",
|
|
199
|
+
/**
|
|
200
|
+
* A row had positive capacity but ZERO eligible campaigns — a PAUSED-only
|
|
201
|
+
* workspace or a sender enrolled in nothing. It is NOT the vacuously-true
|
|
202
|
+
* "every campaign exhausted" and NOT a defect.
|
|
203
|
+
*/
|
|
204
|
+
"no_eligible_campaigns",
|
|
205
|
+
/**
|
|
206
|
+
* Visible unclaimed CAMPAIGN supply with no chosen action. A typed DEFECT and
|
|
207
|
+
* a DEFERRED terminal for queue purposes, never clean exhaustion.
|
|
208
|
+
*/
|
|
209
|
+
"campaign_supply_unclaimed_defect",
|
|
210
|
+
/** Every traversed row already met its capacity for this observation. */
|
|
211
|
+
"daily_targets_met",
|
|
212
|
+
/** The exact requested scope resolved to no sender at all. */
|
|
213
|
+
"scope_unresolved",
|
|
214
|
+
];
|
|
215
|
+
/**
|
|
216
|
+
* Codes emitted ONLY by the coordinator. They EXTEND the fill-ready vocabulary,
|
|
217
|
+
* which already extends the world-state vocabulary, so retryability stays a
|
|
218
|
+
* property of the code rather than of a call site.
|
|
219
|
+
*/
|
|
220
|
+
export const REFILL_V3_ADVANCE_ONLY_REASON_CODES = [
|
|
221
|
+
/** Recheck is impossible without an account id, so it is never attempted. */
|
|
222
|
+
"account_identity_missing",
|
|
223
|
+
/** A success-shaped receipt produced ZERO product-state delta. */
|
|
224
|
+
"no_progress_refused",
|
|
225
|
+
/** Two matching retryable paid failures already exist for this scope. */
|
|
226
|
+
"retryable_paid_attempts_exhausted",
|
|
227
|
+
/** A closed vocabulary carried a value this version does not understand. */
|
|
228
|
+
"unknown_transport_vocabulary",
|
|
229
|
+
/** Receipt facts did not match the observation a server load produced. */
|
|
230
|
+
"forged_authority_receipt_facts",
|
|
231
|
+
/** An authority threw or timed out; the effect is `uncertain`, not a no-op. */
|
|
232
|
+
"authority_failure_uncertain",
|
|
233
|
+
/** An exact reread whose sender no longer resolves. Terminal for that scope. */
|
|
234
|
+
"sender_scope_unresolved",
|
|
235
|
+
/** Unclaimed campaign supply the coordinator could not turn into an action. */
|
|
236
|
+
"campaign_supply_unclaimed",
|
|
237
|
+
/** Every eligible campaign for this sender/lane is receipt-proven spent. */
|
|
238
|
+
"lane_campaigns_exhausted",
|
|
239
|
+
/**
|
|
240
|
+
* The paid entitlement is ABSENT. It is a distinct fact from a zero balance and
|
|
241
|
+
* from a disconnected account, so it is never coerced into either.
|
|
242
|
+
*/
|
|
243
|
+
"missing_balance",
|
|
244
|
+
/** A recorded permanent business blocker with no more specific code. */
|
|
245
|
+
"blocked",
|
|
246
|
+
/**
|
|
247
|
+
* Membership was revoked BETWEEN the fresh read and the mutation. The
|
|
248
|
+
* coordinator re-asserts membership inside the mutation path precisely so this
|
|
249
|
+
* is a refusal rather than an executed mutation.
|
|
250
|
+
*/
|
|
251
|
+
"workspace_membership_revoked",
|
|
252
|
+
];
|
|
253
|
+
export const REFILL_V3_ADVANCE_REASON_CODES = [
|
|
254
|
+
...REFILL_V3_FILL_READY_REASON_CODES,
|
|
255
|
+
...REFILL_V3_ADVANCE_ONLY_REASON_CODES,
|
|
256
|
+
];
|
|
257
|
+
export const REFILL_V3_ADVANCE_RECEIPT_STATUSES = [
|
|
258
|
+
"executed",
|
|
259
|
+
"attached",
|
|
260
|
+
"failed",
|
|
261
|
+
"indeterminate",
|
|
262
|
+
];
|
|
263
|
+
/**
|
|
264
|
+
* Identity, payload, and bookkeeping classes NO result variant may carry at its
|
|
265
|
+
* OWN level. A raw row/cell/provider payload would move the table into the
|
|
266
|
+
* caller; a retry counter or cursor would recreate the deleted state machine.
|
|
267
|
+
*
|
|
268
|
+
* `tableId` is deliberately ABSENT from this list: the Plan 02 packet contract
|
|
269
|
+
* owns `packet.scope.tableId` as a required field, so banning it here would make
|
|
270
|
+
* the two contracts mutually unsatisfiable. Every other identity class stays
|
|
271
|
+
* banned, and the packet's own forbidden-key list governs the packet.
|
|
272
|
+
*/
|
|
273
|
+
export const REFILL_V3_ADVANCE_FORBIDDEN_RESULT_KEYS = [
|
|
274
|
+
"rows",
|
|
275
|
+
"cells",
|
|
276
|
+
"rowIds",
|
|
277
|
+
"cellIds",
|
|
278
|
+
"rowPayload",
|
|
279
|
+
"cellPayload",
|
|
280
|
+
"providerResponse",
|
|
281
|
+
"providerPayload",
|
|
282
|
+
"rank",
|
|
283
|
+
"ranking",
|
|
284
|
+
"graph",
|
|
285
|
+
"waterfall",
|
|
286
|
+
"retryCount",
|
|
287
|
+
"attempts",
|
|
288
|
+
"cursor",
|
|
289
|
+
"continuation",
|
|
290
|
+
"prompt",
|
|
291
|
+
"instructions",
|
|
292
|
+
"durationMs",
|
|
293
|
+
"evaluatedAt",
|
|
294
|
+
];
|
|
295
|
+
/** Required keys per variant. The decoder rejects any other key set. */
|
|
296
|
+
export const REFILL_V3_ADVANCE_RESULT_KEYS = Object.freeze({
|
|
297
|
+
advanced: ["kind", "scope", "action", "receipt", "replacementObservation"],
|
|
298
|
+
campaign_attention_required: ["kind", "scope", "packet", "observation"],
|
|
299
|
+
awaiting_external_change: ["kind", "scope", "deferral", "observation"],
|
|
300
|
+
complete: ["kind", "scope", "terminal", "observation"],
|
|
301
|
+
blocked: ["kind", "scope", "blocker", "observation"],
|
|
302
|
+
});
|
|
303
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
304
|
+
// Public output schema
|
|
305
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
306
|
+
const SUPPLY_SCHEMA = {
|
|
307
|
+
type: "object",
|
|
308
|
+
additionalProperties: false,
|
|
309
|
+
required: [...REFILL_V3_SUPPLY_BUCKETS],
|
|
310
|
+
properties: Object.fromEntries(REFILL_V3_SUPPLY_BUCKETS.map((bucket) => [
|
|
311
|
+
bucket,
|
|
312
|
+
{ type: "integer", minimum: 0 },
|
|
313
|
+
])),
|
|
314
|
+
};
|
|
315
|
+
const OBSERVATION_SCHEMA = {
|
|
316
|
+
type: "object",
|
|
317
|
+
additionalProperties: false,
|
|
318
|
+
required: [
|
|
319
|
+
"observationFingerprint",
|
|
320
|
+
"senderCount",
|
|
321
|
+
"rowCount",
|
|
322
|
+
"dates",
|
|
323
|
+
"availableSlots",
|
|
324
|
+
"supply",
|
|
325
|
+
],
|
|
326
|
+
properties: {
|
|
327
|
+
observationFingerprint: { type: "string", minLength: 1 },
|
|
328
|
+
senderCount: { type: "integer", minimum: 0 },
|
|
329
|
+
rowCount: { type: "integer", minimum: 0 },
|
|
330
|
+
dates: {
|
|
331
|
+
type: "array",
|
|
332
|
+
items: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" },
|
|
333
|
+
},
|
|
334
|
+
availableSlots: { type: "integer", minimum: 0 },
|
|
335
|
+
supply: SUPPLY_SCHEMA,
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
const SCOPE_ECHO_SCHEMA = {
|
|
339
|
+
type: "object",
|
|
340
|
+
additionalProperties: false,
|
|
341
|
+
required: ["workspaceId", "scopeKind", "scopeFingerprint"],
|
|
342
|
+
properties: {
|
|
343
|
+
workspaceId: { type: "string", minLength: 1 },
|
|
344
|
+
scopeKind: {
|
|
345
|
+
type: "string",
|
|
346
|
+
enum: ["sender_local_horizon", "exact_sender_dates"],
|
|
347
|
+
},
|
|
348
|
+
scopeFingerprint: { type: "string", minLength: 1 },
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
const ROW_SCOPE_PROPERTIES = {
|
|
352
|
+
senderId: { type: "string", minLength: 1 },
|
|
353
|
+
date: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" },
|
|
354
|
+
lane: { type: "string", enum: [...REFILL_V3_WORLD_STATE_LANES] },
|
|
355
|
+
campaignId: { type: ["string", "null"] },
|
|
356
|
+
};
|
|
357
|
+
/**
|
|
358
|
+
* A TOP-LEVEL OBJECT, never a union of arrays: an MCP client validates
|
|
359
|
+
* `structuredContent` against this declaration, and a tool whose answer the
|
|
360
|
+
* client rejects has its entire response discarded.
|
|
361
|
+
*/
|
|
362
|
+
export const REFILL_V3_ADVANCE_OUTPUT_SCHEMA = Object.freeze({
|
|
363
|
+
type: "object",
|
|
364
|
+
required: ["kind", "scope"],
|
|
365
|
+
properties: {
|
|
366
|
+
schemaVersion: { type: "string" },
|
|
367
|
+
kind: { type: "string", enum: [...REFILL_V3_ADVANCE_RESULT_KINDS] },
|
|
368
|
+
scope: SCOPE_ECHO_SCHEMA,
|
|
369
|
+
action: {
|
|
370
|
+
type: "object",
|
|
371
|
+
additionalProperties: false,
|
|
372
|
+
required: [
|
|
373
|
+
"kind",
|
|
374
|
+
"actionKey",
|
|
375
|
+
"authority",
|
|
376
|
+
"senderId",
|
|
377
|
+
"date",
|
|
378
|
+
"lane",
|
|
379
|
+
"campaignId",
|
|
380
|
+
"evidenceFingerprint",
|
|
381
|
+
],
|
|
382
|
+
properties: {
|
|
383
|
+
kind: { type: "string", enum: [...REFILL_V3_ADVANCE_ACTION_KINDS] },
|
|
384
|
+
actionKey: { type: "string", minLength: 1 },
|
|
385
|
+
authority: {
|
|
386
|
+
type: "string",
|
|
387
|
+
enum: Object.values(REFILL_V3_ADVANCE_ACTION_AUTHORITIES),
|
|
388
|
+
},
|
|
389
|
+
...ROW_SCOPE_PROPERTIES,
|
|
390
|
+
evidenceFingerprint: { type: "string", minLength: 1 },
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
receipt: {
|
|
394
|
+
type: "object",
|
|
395
|
+
additionalProperties: false,
|
|
396
|
+
required: [
|
|
397
|
+
"actionKey",
|
|
398
|
+
"status",
|
|
399
|
+
"effectFingerprint",
|
|
400
|
+
"productStateDelta",
|
|
401
|
+
"reasonCode",
|
|
402
|
+
],
|
|
403
|
+
properties: {
|
|
404
|
+
actionKey: { type: "string", minLength: 1 },
|
|
405
|
+
status: {
|
|
406
|
+
type: "string",
|
|
407
|
+
enum: [...REFILL_V3_ADVANCE_RECEIPT_STATUSES],
|
|
408
|
+
},
|
|
409
|
+
effectFingerprint: { type: "string", minLength: 1 },
|
|
410
|
+
productStateDelta: { type: "boolean" },
|
|
411
|
+
reasonCode: {
|
|
412
|
+
type: ["string", "null"],
|
|
413
|
+
enum: [...REFILL_V3_ADVANCE_REASON_CODES, null],
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
// The Plan 02 packet schema, plus the ONE transport key its encoder attaches.
|
|
418
|
+
// The packet's own `additionalProperties: false` still governs its business
|
|
419
|
+
// fields; widening it here for `schemaVersion` only is what keeps the two
|
|
420
|
+
// contracts mutually satisfiable instead of mutually exclusive.
|
|
421
|
+
packet: {
|
|
422
|
+
...REFILL_V3_CAMPAIGN_ATTENTION_PACKET_SCHEMA,
|
|
423
|
+
properties: {
|
|
424
|
+
...REFILL_V3_CAMPAIGN_ATTENTION_PACKET_SCHEMA.properties,
|
|
425
|
+
schemaVersion: { type: "string" },
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
deferral: {
|
|
429
|
+
type: "object",
|
|
430
|
+
additionalProperties: false,
|
|
431
|
+
required: [
|
|
432
|
+
"senderId",
|
|
433
|
+
"date",
|
|
434
|
+
"lane",
|
|
435
|
+
"campaignId",
|
|
436
|
+
"code",
|
|
437
|
+
"message",
|
|
438
|
+
"resumeAt",
|
|
439
|
+
"receiptRef",
|
|
440
|
+
],
|
|
441
|
+
properties: {
|
|
442
|
+
...ROW_SCOPE_PROPERTIES,
|
|
443
|
+
code: { type: "string", enum: [...REFILL_V3_ADVANCE_REASON_CODES] },
|
|
444
|
+
message: { type: "string" },
|
|
445
|
+
resumeAt: { type: ["string", "null"] },
|
|
446
|
+
receiptRef: { type: ["string", "null"] },
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
blocker: {
|
|
450
|
+
type: "object",
|
|
451
|
+
additionalProperties: false,
|
|
452
|
+
required: [
|
|
453
|
+
"senderId",
|
|
454
|
+
"date",
|
|
455
|
+
"lane",
|
|
456
|
+
"campaignId",
|
|
457
|
+
"code",
|
|
458
|
+
"message",
|
|
459
|
+
"permanent",
|
|
460
|
+
"field",
|
|
461
|
+
],
|
|
462
|
+
properties: {
|
|
463
|
+
...ROW_SCOPE_PROPERTIES,
|
|
464
|
+
code: { type: "string", enum: [...REFILL_V3_ADVANCE_REASON_CODES] },
|
|
465
|
+
message: { type: "string" },
|
|
466
|
+
permanent: { type: "boolean" },
|
|
467
|
+
field: { type: ["string", "null"] },
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
terminal: {
|
|
471
|
+
type: "string",
|
|
472
|
+
enum: [...REFILL_V3_ADVANCE_TERMINAL_CODES],
|
|
473
|
+
},
|
|
474
|
+
observation: OBSERVATION_SCHEMA,
|
|
475
|
+
replacementObservation: OBSERVATION_SCHEMA,
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
479
|
+
// Sole unknown-value decoder
|
|
480
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
481
|
+
function isRecord(value) {
|
|
482
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
483
|
+
}
|
|
484
|
+
function isOneOfAdvance(value, allowed) {
|
|
485
|
+
return (typeof value === "string" && allowed.includes(value));
|
|
486
|
+
}
|
|
487
|
+
const ADVANCE_TRANSPORT_KEYS = ["schemaVersion"];
|
|
488
|
+
const CANONICAL_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
489
|
+
function onlyKeys(record, allowed) {
|
|
490
|
+
return Object.keys(record).every((key) => allowed.includes(key));
|
|
491
|
+
}
|
|
492
|
+
function nonEmptyString(value) {
|
|
493
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
494
|
+
}
|
|
495
|
+
function count(value) {
|
|
496
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
497
|
+
}
|
|
498
|
+
function decodeSupply(value) {
|
|
499
|
+
if (!isRecord(value) || !onlyKeys(value, REFILL_V3_SUPPLY_BUCKETS)) {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
if (REFILL_V3_SUPPLY_BUCKETS.some((bucket) => !count(value[bucket]))) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
readyToSchedule: value.readyToSchedule,
|
|
507
|
+
readyForApproval: value.readyForApproval,
|
|
508
|
+
readyForMessageGeneration: value.readyForMessageGeneration,
|
|
509
|
+
needsEnrichment: value.needsEnrichment,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function decodeObservation(value) {
|
|
513
|
+
if (!isRecord(value))
|
|
514
|
+
return null;
|
|
515
|
+
const keys = [
|
|
516
|
+
"observationFingerprint",
|
|
517
|
+
"senderCount",
|
|
518
|
+
"rowCount",
|
|
519
|
+
"dates",
|
|
520
|
+
"availableSlots",
|
|
521
|
+
"supply",
|
|
522
|
+
];
|
|
523
|
+
if (!onlyKeys(value, keys) || keys.some((key) => !(key in value))) {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
if (!nonEmptyString(value.observationFingerprint) ||
|
|
527
|
+
!count(value.senderCount) ||
|
|
528
|
+
!count(value.rowCount) ||
|
|
529
|
+
!count(value.availableSlots) ||
|
|
530
|
+
!Array.isArray(value.dates) ||
|
|
531
|
+
value.dates.some((date) => typeof date !== "string" || !CANONICAL_DATE.test(date))) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
const supply = decodeSupply(value.supply);
|
|
535
|
+
if (!supply)
|
|
536
|
+
return null;
|
|
537
|
+
return {
|
|
538
|
+
observationFingerprint: value.observationFingerprint,
|
|
539
|
+
senderCount: value.senderCount,
|
|
540
|
+
rowCount: value.rowCount,
|
|
541
|
+
dates: [...value.dates],
|
|
542
|
+
availableSlots: value.availableSlots,
|
|
543
|
+
supply,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function decodeScopeEcho(value) {
|
|
547
|
+
if (!isRecord(value))
|
|
548
|
+
return null;
|
|
549
|
+
const keys = ["workspaceId", "scopeKind", "scopeFingerprint"];
|
|
550
|
+
if (!onlyKeys(value, keys) || keys.some((key) => !(key in value)))
|
|
551
|
+
return null;
|
|
552
|
+
if (!nonEmptyString(value.workspaceId) ||
|
|
553
|
+
!nonEmptyString(value.scopeFingerprint) ||
|
|
554
|
+
!isOneOfAdvance(value.scopeKind, [
|
|
555
|
+
"sender_local_horizon",
|
|
556
|
+
"exact_sender_dates",
|
|
557
|
+
])) {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
workspaceId: value.workspaceId,
|
|
562
|
+
scopeKind: value.scopeKind,
|
|
563
|
+
scopeFingerprint: value.scopeFingerprint,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function decodeRowScope(value) {
|
|
567
|
+
if (!nonEmptyString(value.senderId) ||
|
|
568
|
+
typeof value.date !== "string" ||
|
|
569
|
+
!CANONICAL_DATE.test(value.date) ||
|
|
570
|
+
!isOneOfAdvance(value.lane, REFILL_V3_WORLD_STATE_LANES) ||
|
|
571
|
+
(value.campaignId !== null && !nonEmptyString(value.campaignId))) {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
senderId: value.senderId,
|
|
576
|
+
date: value.date,
|
|
577
|
+
lane: value.lane,
|
|
578
|
+
campaignId: value.campaignId,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function decodeAction(value) {
|
|
582
|
+
if (!isRecord(value))
|
|
583
|
+
return null;
|
|
584
|
+
const keys = [
|
|
585
|
+
"kind",
|
|
586
|
+
"actionKey",
|
|
587
|
+
"authority",
|
|
588
|
+
"senderId",
|
|
589
|
+
"date",
|
|
590
|
+
"lane",
|
|
591
|
+
"campaignId",
|
|
592
|
+
"evidenceFingerprint",
|
|
593
|
+
];
|
|
594
|
+
if (!onlyKeys(value, keys) || keys.some((key) => !(key in value)))
|
|
595
|
+
return null;
|
|
596
|
+
const rowScope = decodeRowScope(value);
|
|
597
|
+
if (!rowScope)
|
|
598
|
+
return null;
|
|
599
|
+
if (!isOneOfAdvance(value.kind, REFILL_V3_ADVANCE_ACTION_KINDS) ||
|
|
600
|
+
!nonEmptyString(value.actionKey) ||
|
|
601
|
+
!nonEmptyString(value.evidenceFingerprint)) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
// The authority is a CLOSED map lookup, so a mismatched pair is malformed
|
|
605
|
+
// transport rather than a plausible action.
|
|
606
|
+
if (value.authority !== REFILL_V3_ADVANCE_ACTION_AUTHORITIES[value.kind]) {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
kind: value.kind,
|
|
611
|
+
actionKey: value.actionKey,
|
|
612
|
+
authority: REFILL_V3_ADVANCE_ACTION_AUTHORITIES[value.kind],
|
|
613
|
+
...rowScope,
|
|
614
|
+
evidenceFingerprint: value.evidenceFingerprint,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function decodeReceipt(value) {
|
|
618
|
+
if (!isRecord(value))
|
|
619
|
+
return null;
|
|
620
|
+
const keys = [
|
|
621
|
+
"actionKey",
|
|
622
|
+
"status",
|
|
623
|
+
"effectFingerprint",
|
|
624
|
+
"productStateDelta",
|
|
625
|
+
"reasonCode",
|
|
626
|
+
];
|
|
627
|
+
if (!onlyKeys(value, keys) || keys.some((key) => !(key in value)))
|
|
628
|
+
return null;
|
|
629
|
+
if (!nonEmptyString(value.actionKey) ||
|
|
630
|
+
!isOneOfAdvance(value.status, REFILL_V3_ADVANCE_RECEIPT_STATUSES) ||
|
|
631
|
+
!nonEmptyString(value.effectFingerprint) ||
|
|
632
|
+
typeof value.productStateDelta !== "boolean") {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
if (value.reasonCode !== null &&
|
|
636
|
+
!isOneOfAdvance(value.reasonCode, REFILL_V3_ADVANCE_REASON_CODES)) {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
actionKey: value.actionKey,
|
|
641
|
+
status: value.status,
|
|
642
|
+
effectFingerprint: value.effectFingerprint,
|
|
643
|
+
productStateDelta: value.productStateDelta,
|
|
644
|
+
reasonCode: value.reasonCode,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function decodeDeferral(value) {
|
|
648
|
+
if (!isRecord(value))
|
|
649
|
+
return null;
|
|
650
|
+
const keys = [
|
|
651
|
+
"senderId",
|
|
652
|
+
"date",
|
|
653
|
+
"lane",
|
|
654
|
+
"campaignId",
|
|
655
|
+
"code",
|
|
656
|
+
"message",
|
|
657
|
+
"resumeAt",
|
|
658
|
+
"receiptRef",
|
|
659
|
+
];
|
|
660
|
+
if (!onlyKeys(value, keys) || keys.some((key) => !(key in value)))
|
|
661
|
+
return null;
|
|
662
|
+
const rowScope = decodeRowScope(value);
|
|
663
|
+
if (!rowScope)
|
|
664
|
+
return null;
|
|
665
|
+
if (!isOneOfAdvance(value.code, REFILL_V3_ADVANCE_REASON_CODES) ||
|
|
666
|
+
typeof value.message !== "string" ||
|
|
667
|
+
(value.resumeAt !== null && !nonEmptyString(value.resumeAt)) ||
|
|
668
|
+
(value.receiptRef !== null && !nonEmptyString(value.receiptRef))) {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
...rowScope,
|
|
673
|
+
code: value.code,
|
|
674
|
+
message: value.message,
|
|
675
|
+
resumeAt: value.resumeAt,
|
|
676
|
+
receiptRef: value.receiptRef,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function decodeBlocker(value) {
|
|
680
|
+
if (!isRecord(value))
|
|
681
|
+
return null;
|
|
682
|
+
const keys = [
|
|
683
|
+
"senderId",
|
|
684
|
+
"date",
|
|
685
|
+
"lane",
|
|
686
|
+
"campaignId",
|
|
687
|
+
"code",
|
|
688
|
+
"message",
|
|
689
|
+
"permanent",
|
|
690
|
+
"field",
|
|
691
|
+
];
|
|
692
|
+
if (!onlyKeys(value, keys) || keys.some((key) => !(key in value)))
|
|
693
|
+
return null;
|
|
694
|
+
const rowScope = decodeRowScope(value);
|
|
695
|
+
if (!rowScope)
|
|
696
|
+
return null;
|
|
697
|
+
if (!isOneOfAdvance(value.code, REFILL_V3_ADVANCE_REASON_CODES) ||
|
|
698
|
+
typeof value.message !== "string" ||
|
|
699
|
+
typeof value.permanent !== "boolean" ||
|
|
700
|
+
(value.field !== null && !nonEmptyString(value.field))) {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
...rowScope,
|
|
705
|
+
code: value.code,
|
|
706
|
+
message: value.message,
|
|
707
|
+
permanent: value.permanent,
|
|
708
|
+
field: value.field,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Sole unknown-value decoder for the public advance result.
|
|
713
|
+
*
|
|
714
|
+
* STRICT and FAIL-CLOSED per variant: the union discriminator selects ONE exact
|
|
715
|
+
* key set, so a result that carries an `advanced` payload under a `complete`
|
|
716
|
+
* kind, an unknown action, a leaked row identity, or a missing replacement
|
|
717
|
+
* observation is rejected outright rather than narrowed into a confident
|
|
718
|
+
* partial success.
|
|
719
|
+
*/
|
|
720
|
+
export function decodeRefillV3AdvanceResult(value) {
|
|
721
|
+
if (!isRecord(value))
|
|
722
|
+
return null;
|
|
723
|
+
if (!isOneOfAdvance(value.kind, REFILL_V3_ADVANCE_RESULT_KINDS))
|
|
724
|
+
return null;
|
|
725
|
+
const required = REFILL_V3_ADVANCE_RESULT_KEYS[value.kind];
|
|
726
|
+
if (!onlyKeys(value, [...required, ...ADVANCE_TRANSPORT_KEYS]))
|
|
727
|
+
return null;
|
|
728
|
+
if (required.some((key) => !(key in value)))
|
|
729
|
+
return null;
|
|
730
|
+
if (value.schemaVersion !== undefined &&
|
|
731
|
+
!resolveRefillV3AdvanceSchemaVersion(value.schemaVersion).supported) {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
const scope = decodeScopeEcho(value.scope);
|
|
735
|
+
if (!scope)
|
|
736
|
+
return null;
|
|
737
|
+
if (value.kind === "advanced") {
|
|
738
|
+
const action = decodeAction(value.action);
|
|
739
|
+
const receipt = decodeReceipt(value.receipt);
|
|
740
|
+
const replacementObservation = decodeObservation(value.replacementObservation);
|
|
741
|
+
if (!action || !receipt || !replacementObservation)
|
|
742
|
+
return null;
|
|
743
|
+
// The receipt must belong to the ACTION it claims to receipt.
|
|
744
|
+
if (receipt.actionKey !== action.actionKey)
|
|
745
|
+
return null;
|
|
746
|
+
return { kind: "advanced", scope, action, receipt, replacementObservation };
|
|
747
|
+
}
|
|
748
|
+
const observation = decodeObservation(value.observation);
|
|
749
|
+
if (!observation)
|
|
750
|
+
return null;
|
|
751
|
+
if (value.kind === "campaign_attention_required") {
|
|
752
|
+
const packet = decodeRefillV3CampaignAttentionPacket(value.packet);
|
|
753
|
+
// The typed unsupported VALUE is a different fact from `null`, but neither is
|
|
754
|
+
// a usable packet for this seam, so both reject the whole result.
|
|
755
|
+
if (packet === null || packet === REFILL_V3_SCHEMA_VERSION_UNSUPPORTED) {
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
return { kind: "campaign_attention_required", scope, packet, observation };
|
|
759
|
+
}
|
|
760
|
+
if (value.kind === "awaiting_external_change") {
|
|
761
|
+
const deferral = decodeDeferral(value.deferral);
|
|
762
|
+
if (!deferral)
|
|
763
|
+
return null;
|
|
764
|
+
return { kind: "awaiting_external_change", scope, deferral, observation };
|
|
765
|
+
}
|
|
766
|
+
if (value.kind === "complete") {
|
|
767
|
+
if (!isOneOfAdvance(value.terminal, REFILL_V3_ADVANCE_TERMINAL_CODES)) {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
return { kind: "complete", scope, terminal: value.terminal, observation };
|
|
771
|
+
}
|
|
772
|
+
const blocker = decodeBlocker(value.blocker);
|
|
773
|
+
if (!blocker)
|
|
774
|
+
return null;
|
|
775
|
+
return { kind: "blocked", scope, blocker, observation };
|
|
776
|
+
}
|
|
777
|
+
/** Wire form of a decoded result. `schemaVersion` is TRANSPORT, not business. */
|
|
778
|
+
export function encodeRefillV3AdvanceResult(result) {
|
|
779
|
+
const base = {
|
|
780
|
+
schemaVersion: REFILL_V3_ADVANCE_SCHEMA_VERSION,
|
|
781
|
+
kind: result.kind,
|
|
782
|
+
scope: { ...result.scope },
|
|
783
|
+
};
|
|
784
|
+
if (result.kind === "advanced") {
|
|
785
|
+
return {
|
|
786
|
+
...base,
|
|
787
|
+
action: { ...result.action },
|
|
788
|
+
receipt: { ...result.receipt },
|
|
789
|
+
replacementObservation: {
|
|
790
|
+
...result.replacementObservation,
|
|
791
|
+
dates: [...result.replacementObservation.dates],
|
|
792
|
+
supply: { ...result.replacementObservation.supply },
|
|
793
|
+
},
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
const observation = {
|
|
797
|
+
...result.observation,
|
|
798
|
+
dates: [...result.observation.dates],
|
|
799
|
+
supply: { ...result.observation.supply },
|
|
800
|
+
};
|
|
801
|
+
if (result.kind === "campaign_attention_required") {
|
|
802
|
+
return {
|
|
803
|
+
...base,
|
|
804
|
+
packet: encodeRefillV3CampaignAttentionPacket(result.packet),
|
|
805
|
+
observation,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
if (result.kind === "awaiting_external_change") {
|
|
809
|
+
return { ...base, deferral: { ...result.deferral }, observation };
|
|
810
|
+
}
|
|
811
|
+
if (result.kind === "complete") {
|
|
812
|
+
return { ...base, terminal: result.terminal, observation };
|
|
813
|
+
}
|
|
814
|
+
return { ...base, blocker: { ...result.blocker }, observation };
|
|
815
|
+
}
|
|
816
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
817
|
+
// The canonical V3 flow asset and its COMPATIBLE-RANGE version gate
|
|
818
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
819
|
+
/**
|
|
820
|
+
* The ONE canonical V3 flow asset. The authoritative consumer is the INSTALLED
|
|
821
|
+
* skill cache, not a file in this repository, so the gate is a compatible RANGE
|
|
822
|
+
* rather than an exact string: an exact pin would halt every installed caller the
|
|
823
|
+
* moment a compatible minor shipped.
|
|
824
|
+
*/
|
|
825
|
+
export const REFILL_V3_FLOW_ASSET = Object.freeze({
|
|
826
|
+
subskillName: "refill-sends-workflow",
|
|
827
|
+
assetPath: "core/flow.v3.json",
|
|
828
|
+
minimumVersion: "v3.0",
|
|
829
|
+
compatibleMajor: "v3",
|
|
830
|
+
versionPolicy: "compatible_range",
|
|
831
|
+
});
|
|
832
|
+
export const REFILL_V3_FLOW_VERSION_VERDICTS = [
|
|
833
|
+
"compatible",
|
|
834
|
+
"incompatible_major",
|
|
835
|
+
"below_minimum_minor",
|
|
836
|
+
"malformed",
|
|
837
|
+
];
|
|
838
|
+
/**
|
|
839
|
+
* The typed flow-asset version gate (error law 3): an incompatible major is a
|
|
840
|
+
* NAMED value, never a bare `false` and never a throw.
|
|
841
|
+
*
|
|
842
|
+
* It carries its own two-integer parser instead of importing
|
|
843
|
+
* `isCompatibleRefillFlowVersion`, because that symbol lives beside the V1 API
|
|
844
|
+
* client and a contract module must stay free of the transport layer. The two are
|
|
845
|
+
* bound by an executable agreement assertion in
|
|
846
|
+
* `tests/mcp/refill-v3/146-flow-v3-contract.ts` rather than by one of them
|
|
847
|
+
* silently forking.
|
|
848
|
+
*/
|
|
849
|
+
export function resolveRefillV3FlowAssetVersion(value, bounds = {}) {
|
|
850
|
+
const parse = (candidate) => {
|
|
851
|
+
if (typeof candidate !== "string")
|
|
852
|
+
return null;
|
|
853
|
+
const match = /^v(\d+)\.(\d+)$/.exec(candidate.trim());
|
|
854
|
+
if (!match)
|
|
855
|
+
return null;
|
|
856
|
+
return { major: Number(match[1]), minor: Number(match[2]) };
|
|
857
|
+
};
|
|
858
|
+
const actual = parse(value);
|
|
859
|
+
if (!actual)
|
|
860
|
+
return { verdict: "malformed" };
|
|
861
|
+
const minimum = parse(bounds.minimumVersion ?? REFILL_V3_FLOW_ASSET.minimumVersion);
|
|
862
|
+
const expectedMajor = parse(`${bounds.compatibleMajor ?? REFILL_V3_FLOW_ASSET.compatibleMajor}.0`);
|
|
863
|
+
if (!minimum || !expectedMajor)
|
|
864
|
+
return { verdict: "malformed" };
|
|
865
|
+
if (actual.major !== expectedMajor.major) {
|
|
866
|
+
return { verdict: "incompatible_major", ...actual };
|
|
867
|
+
}
|
|
868
|
+
if (actual.minor < minimum.minor) {
|
|
869
|
+
return { verdict: "below_minimum_minor", ...actual };
|
|
870
|
+
}
|
|
871
|
+
return { verdict: "compatible", ...actual };
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Tokens no thin caller may contain. Each is a DECISION the deterministic plane
|
|
875
|
+
* owns: choosing a row, a campaign, a lane, an action, a terminal, or a retry.
|
|
876
|
+
*/
|
|
877
|
+
export const REFILL_V3_FLOW_FORBIDDEN_CALLER_TOKENS = [
|
|
878
|
+
"availableSlots",
|
|
879
|
+
"readyToSchedule",
|
|
880
|
+
"readyForApproval",
|
|
881
|
+
"needsEnrichment",
|
|
882
|
+
"leadSourceFamily",
|
|
883
|
+
"post_engager",
|
|
884
|
+
"signal_discovery",
|
|
885
|
+
"active_campaigns_exhausted",
|
|
886
|
+
"paid_inmail_connection_health_recheck_required",
|
|
887
|
+
"recheck_sender_connection",
|
|
888
|
+
"refresh_paid_credit_fact",
|
|
889
|
+
"refill_v3_fill_ready",
|
|
890
|
+
"retryCount",
|
|
891
|
+
"cursor",
|
|
892
|
+
"continuation",
|
|
893
|
+
];
|
|
894
|
+
/**
|
|
895
|
+
* The RETIRED public V3 names and the exact replacement an old caller must use.
|
|
896
|
+
*
|
|
897
|
+
* This covers the REAL skew, not just the new package: an already-installed skill
|
|
898
|
+
* or a cached flow asset can still invoke a removed tool name against the new
|
|
899
|
+
* server. A bare "Unknown tool" is an opaque dead end, so the refusal names the
|
|
900
|
+
* one surviving observation surface instead.
|
|
901
|
+
*/
|
|
902
|
+
export const REFILL_V3_RETIRED_TOOL_REPLACEMENTS = Object.freeze({
|
|
903
|
+
refill_v3_workspace_inventory: "refill_v3_world_state",
|
|
904
|
+
refill_v3_sender_date_eligibility: "refill_v3_world_state",
|
|
905
|
+
refill_v3_supply_assessment: "refill_v3_world_state",
|
|
906
|
+
});
|
|
907
|
+
/**
|
|
908
|
+
* The typed unknown-tool refusal message for a retired V3 name. It returns a
|
|
909
|
+
* VALUE, never a throw of its own, so the caller gets an actionable replacement
|
|
910
|
+
* with zero partial dispatch.
|
|
911
|
+
*/
|
|
912
|
+
export function refillV3RetiredToolRefusal(name) {
|
|
913
|
+
const replacement = REFILL_V3_RETIRED_TOOL_REPLACEMENTS[name];
|
|
914
|
+
return replacement
|
|
915
|
+
? `Unknown tool: ${name}. It was retired in Refill V3; use ${replacement} instead, which returns the whole workspace observation in one call.`
|
|
916
|
+
: null;
|
|
917
|
+
}
|
|
918
|
+
/** The two lanes and the closed family list, re-exported for one-import use. */
|
|
919
|
+
export const REFILL_V3_ADVANCE_LANES = REFILL_V3_WORLD_STATE_LANES;
|
|
920
|
+
export const REFILL_V3_ADVANCE_LEAD_SOURCE_FAMILIES = REFILL_V3_LEAD_SOURCE_FAMILIES;
|