@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,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refill V3 table-driven SOURCE-FAMILY DISPATCHER (Phase 146.5 Plan 09 T3/T4/T5,
|
|
3
|
+
* RV3PRM-002/003/004).
|
|
4
|
+
*
|
|
5
|
+
* EXECUTION PLUMBING ONLY. The AI seam decides WHY to broaden, which family,
|
|
6
|
+
* which contextual query, and which minimal filter/keyword/post delta. This
|
|
7
|
+
* module validates that already-bounded command and maps it, through a CONSTANT
|
|
8
|
+
* table, to exactly one existing owner. It contains no family inference, no
|
|
9
|
+
* query invention, no ranking, no yield projection, no provider retry, no import
|
|
10
|
+
* policy, and no fallback between families.
|
|
11
|
+
*
|
|
12
|
+
* It lives INSIDE the package beside its owners (`A09-DISPATCHER-IN-PACKAGE`):
|
|
13
|
+
* every named family owner is declared in `refill-executors.ts`, which reaches
|
|
14
|
+
* the backend over HTTP, so an app-side dispatcher would create an
|
|
15
|
+
* app -> MCP-api-client -> app-route loop and needs a route that was never
|
|
16
|
+
* defined.
|
|
17
|
+
*
|
|
18
|
+
* The searched-negative set is DERIVED from existing source-run/import lineage
|
|
19
|
+
* plus effect fingerprints (`A09-NEGATIVE-SET-OWNER`). There is no new database
|
|
20
|
+
* model, no attempt counter, and no durable ledger. Every entry is append-only,
|
|
21
|
+
* fingerprint-identified, and carries an explicit typed RELEASE CONDITION, so a
|
|
22
|
+
* bounded attempt can never harden into a standing claim.
|
|
23
|
+
*/
|
|
24
|
+
import { evaluateRefillV3SourceYield, REFILL_V3_EXHAUSTION_PREDICATE, REFILL_V3_LEAD_SOURCE_FAMILIES, REFILL_V3_MIN_SOURCE_YIELD, REFILL_V3_MIN_SOURCE_YIELD_SAMPLE, } from "./refill-v3-attention-packet-contract.js";
|
|
25
|
+
import { REFILL_V3_POSTS_KEYWORD_BATCH_SIZE, REFILL_V3_POSTS_MAX_SELECTED, REFILL_V3_POSTS_ROUND_BUDGET, } from "./refill-v3-campaign-work.js";
|
|
26
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
27
|
+
// The constant owner table
|
|
28
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
29
|
+
export const REFILL_V3_SOURCE_OWNER_KEYS = [
|
|
30
|
+
"refreshSenderPostEngagers",
|
|
31
|
+
"continueSignalDiscoverySource",
|
|
32
|
+
"broadenSignalSearch",
|
|
33
|
+
"continueProviderSource",
|
|
34
|
+
"broadenProviderSearch",
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* The CONSTANT family table. The public token `cold` maps INTERNALLY to the
|
|
38
|
+
* provider owners; no boundary rewrites it to another public token.
|
|
39
|
+
*/
|
|
40
|
+
export const REFILL_V3_SOURCE_FAMILY_TABLE = Object.freeze({
|
|
41
|
+
post_engager: {
|
|
42
|
+
continuation: "refreshSenderPostEngagers",
|
|
43
|
+
broadening: "refreshSenderPostEngagers",
|
|
44
|
+
},
|
|
45
|
+
signal_discovery: {
|
|
46
|
+
continuation: "continueSignalDiscoverySource",
|
|
47
|
+
broadening: "broadenSignalSearch",
|
|
48
|
+
},
|
|
49
|
+
cold: {
|
|
50
|
+
continuation: "continueProviderSource",
|
|
51
|
+
broadening: "broadenProviderSearch",
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
export const REFILL_V3_SOURCE_DISPATCH_REFUSALS = [
|
|
55
|
+
"missing_scope",
|
|
56
|
+
"foreign_scope",
|
|
57
|
+
"unsupported_family",
|
|
58
|
+
"unsupported_action",
|
|
59
|
+
"malformed_command",
|
|
60
|
+
"stale_prior_fingerprint",
|
|
61
|
+
"missing_count",
|
|
62
|
+
"health_evidence_not_source_work",
|
|
63
|
+
"live_source_job",
|
|
64
|
+
"negative_set_replay",
|
|
65
|
+
"incidental_change_only",
|
|
66
|
+
"negative_set_provenance_unavailable",
|
|
67
|
+
"family_switch_prompt_missing",
|
|
68
|
+
"family_switch_negative_set",
|
|
69
|
+
"post_engager_refresh_required",
|
|
70
|
+
"exhaustion_falsified",
|
|
71
|
+
"keyword_batch_size",
|
|
72
|
+
"post_cap_exceeded",
|
|
73
|
+
"post_rationale_missing",
|
|
74
|
+
"posts_round_budget_spent",
|
|
75
|
+
"receipt_incomplete",
|
|
76
|
+
];
|
|
77
|
+
/**
|
|
78
|
+
* Provider INFRASTRUCTURE failure is never exhaustion. Each class is RETRYABLE,
|
|
79
|
+
* leaves the campaign NON-exhausted, and — critically — never enters the
|
|
80
|
+
* searched-negative set, because nothing was actually searched (repairs C18).
|
|
81
|
+
*/
|
|
82
|
+
export const REFILL_V3_SOURCE_FAILURE_CLASSES = [
|
|
83
|
+
"owner_threw",
|
|
84
|
+
"timeout",
|
|
85
|
+
"provider_5xx",
|
|
86
|
+
"rate_limit",
|
|
87
|
+
];
|
|
88
|
+
/**
|
|
89
|
+
* The ONE enumerated release-condition vocabulary shared by the negative-set
|
|
90
|
+
* WRITER and READER. Every entry must name a condition that is actually checked
|
|
91
|
+
* on every evidence change, or the set grows monotonically until every plausible
|
|
92
|
+
* query is refused and the campaign becomes permanently un-broadenable while
|
|
93
|
+
* exhaustion may still be falsified (`A09-NEGATIVE-SET-LIFECYCLE`).
|
|
94
|
+
*/
|
|
95
|
+
export const REFILL_V3_NEGATIVE_SET_RELEASE_CONDITIONS = [
|
|
96
|
+
"campaign_icp_changed",
|
|
97
|
+
"provider_inventory_refreshed",
|
|
98
|
+
"evidence_window_rolled",
|
|
99
|
+
"posts_refreshed_in_window",
|
|
100
|
+
];
|
|
101
|
+
export const REFILL_V3_NEGATIVE_SET_PROVENANCE_UNAVAILABLE = "negative_set_provenance_unavailable";
|
|
102
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
103
|
+
// Command, owners, receipts
|
|
104
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
105
|
+
export const REFILL_V3_SOURCE_DISPATCH_ACTIONS = [
|
|
106
|
+
"continue",
|
|
107
|
+
"broaden",
|
|
108
|
+
"record_exhaustion",
|
|
109
|
+
];
|
|
110
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
111
|
+
// Canonical semantic identity
|
|
112
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
113
|
+
function canonicalScalar(value) {
|
|
114
|
+
if (value === null || value === undefined)
|
|
115
|
+
return "";
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
// A reordered but equivalent list is the SAME search: order is incidental.
|
|
118
|
+
return [...value.map((entry) => canonicalScalar(entry))].sort().join(",");
|
|
119
|
+
}
|
|
120
|
+
if (typeof value === "object") {
|
|
121
|
+
return Object.keys(value)
|
|
122
|
+
.sort()
|
|
123
|
+
.map((key) => `${key}=${canonicalScalar(value[key])}`)
|
|
124
|
+
.join(";");
|
|
125
|
+
}
|
|
126
|
+
return String(value).trim().replace(/\s+/g, " ").toLowerCase();
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The SEMANTIC identity of a source command.
|
|
130
|
+
*
|
|
131
|
+
* A timestamp, a counter, a reordered-but-equivalent filter list, and a
|
|
132
|
+
* whitespace edit are INCIDENTAL: they must not buy a second identical search.
|
|
133
|
+
* The caller's `requestFingerprint` is deliberately excluded — it is a transport
|
|
134
|
+
* identity, and including it would let a cosmetic edit re-run the same query.
|
|
135
|
+
*/
|
|
136
|
+
export function canonicalRefillV3SourceFingerprint(command) {
|
|
137
|
+
return [
|
|
138
|
+
command.family,
|
|
139
|
+
command.mode,
|
|
140
|
+
canonicalScalar(command.provider),
|
|
141
|
+
`q:${canonicalScalar(command.query)}`,
|
|
142
|
+
`f:${canonicalScalar(command.filterDelta)}`,
|
|
143
|
+
`k:${canonicalScalar(command.keywordBatch)}`,
|
|
144
|
+
`p:${canonicalScalar(command.selectedPostIds)}`,
|
|
145
|
+
].join("|");
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Which typed condition RELEASES an entry. Every value is checked on an evidence
|
|
149
|
+
* change, so the set cannot grow monotonically until the campaign is permanently
|
|
150
|
+
* un-broadenable.
|
|
151
|
+
*/
|
|
152
|
+
function releaseConditionFor(record) {
|
|
153
|
+
if (record.family === "post_engager")
|
|
154
|
+
return "posts_refreshed_in_window";
|
|
155
|
+
if (record.keywordBatch !== null || record.postIds !== null) {
|
|
156
|
+
return "evidence_window_rolled";
|
|
157
|
+
}
|
|
158
|
+
return record.provider === null
|
|
159
|
+
? "campaign_icp_changed"
|
|
160
|
+
: "provider_inventory_refreshed";
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Derive the searched-negative set from durable lineage ALONE.
|
|
164
|
+
*
|
|
165
|
+
* Because it is a pure function of canonical lineage, a cold restart and a new
|
|
166
|
+
* day reconstruct identical verdicts and process memory is never load-bearing.
|
|
167
|
+
* `null` lineage is NOT an empty set: silent shrinkage re-admits previously
|
|
168
|
+
* refused searches, so it yields the typed provenance marker instead.
|
|
169
|
+
*/
|
|
170
|
+
export function deriveRefillV3SearchedNegativeSet(lineage, options) {
|
|
171
|
+
if (lineage === null || lineage === undefined) {
|
|
172
|
+
return {
|
|
173
|
+
provenance: REFILL_V3_NEGATIVE_SET_PROVENANCE_UNAVAILABLE,
|
|
174
|
+
entries: [],
|
|
175
|
+
postsRoundsByCampaign: {},
|
|
176
|
+
dryFamilies: [],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const scoped = options?.campaignId
|
|
180
|
+
? lineage.filter((record) => record.campaignId === options.campaignId)
|
|
181
|
+
: lineage;
|
|
182
|
+
const entries = [];
|
|
183
|
+
const postsRoundsByCampaign = {};
|
|
184
|
+
const dryFamilies = [];
|
|
185
|
+
const productive = new Set();
|
|
186
|
+
for (const record of scoped) {
|
|
187
|
+
// Only a COMPLETED search with an observed yield may register.
|
|
188
|
+
if (!record.searchCompleted)
|
|
189
|
+
continue;
|
|
190
|
+
entries.push({
|
|
191
|
+
campaignId: record.campaignId,
|
|
192
|
+
family: record.family,
|
|
193
|
+
sourceFingerprint: record.sourceFingerprint,
|
|
194
|
+
semanticFingerprint: record.semanticFingerprint,
|
|
195
|
+
keywordBatch: record.keywordBatch,
|
|
196
|
+
postIds: record.postIds,
|
|
197
|
+
releaseCondition: releaseConditionFor(record),
|
|
198
|
+
observedAt: record.observedAt,
|
|
199
|
+
});
|
|
200
|
+
if (record.keywordBatch !== null) {
|
|
201
|
+
postsRoundsByCampaign[record.campaignId] =
|
|
202
|
+
(postsRoundsByCampaign[record.campaignId] ?? 0) + 1;
|
|
203
|
+
}
|
|
204
|
+
const familyKey = `${record.family}:${record.campaignId}`;
|
|
205
|
+
if (record.importedCount > 0) {
|
|
206
|
+
productive.add(familyKey);
|
|
207
|
+
}
|
|
208
|
+
else if (!dryFamilies.includes(familyKey)) {
|
|
209
|
+
dryFamilies.push(familyKey);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
provenance: "derived",
|
|
214
|
+
entries,
|
|
215
|
+
postsRoundsByCampaign,
|
|
216
|
+
// Dry is keyed per `(family, campaignId)`: one exhausted family may never
|
|
217
|
+
// suppress another (repairs C17 and error law 9).
|
|
218
|
+
dryFamilies: dryFamilies.filter((key) => !productive.has(key)),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
222
|
+
// Dispatch
|
|
223
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
224
|
+
function refused(refusal, detail, extra = {}) {
|
|
225
|
+
return {
|
|
226
|
+
kind: "refused",
|
|
227
|
+
refusal,
|
|
228
|
+
detail,
|
|
229
|
+
matchingFingerprint: extra.matchingFingerprint ?? null,
|
|
230
|
+
releaseCondition: extra.releaseCondition ?? null,
|
|
231
|
+
exhausted: false,
|
|
232
|
+
reobserveRequired: extra.reobserveRequired ?? true,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function isNonEmpty(value) {
|
|
236
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
237
|
+
}
|
|
238
|
+
function isPlainRecord(value) {
|
|
239
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Validate ONE already-bounded AI command and map it through the constant table
|
|
243
|
+
* to exactly one existing owner.
|
|
244
|
+
*
|
|
245
|
+
* Every refusal happens BEFORE the single owner call, so a refused command costs
|
|
246
|
+
* zero external work. Nothing here chooses a family, a query, a filter, a post,
|
|
247
|
+
* a provider, or a retry.
|
|
248
|
+
*/
|
|
249
|
+
export async function dispatchRefillV3SourceFamily(input) {
|
|
250
|
+
const { scope, command, action } = input;
|
|
251
|
+
// 1. Connection health belongs to Plan 10. It is never source work.
|
|
252
|
+
if ((input.healthEvidence ?? []).length > 0) {
|
|
253
|
+
return refused("health_evidence_not_source_work", `Connection-health evidence (${(input.healthEvidence ?? []).join(", ")}) can never be answered with a source search.`);
|
|
254
|
+
}
|
|
255
|
+
// 2. Exact scope.
|
|
256
|
+
for (const key of [
|
|
257
|
+
"workspaceId",
|
|
258
|
+
"campaignId",
|
|
259
|
+
"tableId",
|
|
260
|
+
"senderId",
|
|
261
|
+
"date",
|
|
262
|
+
"evidenceFingerprint",
|
|
263
|
+
"priorSourceFingerprint",
|
|
264
|
+
"idempotencyKey",
|
|
265
|
+
]) {
|
|
266
|
+
if (!isNonEmpty(scope[key])) {
|
|
267
|
+
return refused("missing_scope", `Source dispatch requires an exact ${key}.`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (!Number.isInteger(scope.count) || scope.count < 0) {
|
|
271
|
+
return refused("missing_count", `Source dispatch requires a whole non-negative count; got ${scope.count}.`);
|
|
272
|
+
}
|
|
273
|
+
if (action !== "record_exhaustion" &&
|
|
274
|
+
command.family !== "post_engager" &&
|
|
275
|
+
!isNonEmpty(scope.sourceLeadListId)) {
|
|
276
|
+
return refused("missing_scope", `Source dispatch for ${command.family} requires the campaign's exact selected source lead-list id.`);
|
|
277
|
+
}
|
|
278
|
+
// 3. Closed vocabularies.
|
|
279
|
+
if (!isRefillV3LeadSourceFamily(command.family)) {
|
|
280
|
+
return refused("unsupported_family", `${String(command.family)} is outside the closed family table.`);
|
|
281
|
+
}
|
|
282
|
+
if (!REFILL_V3_SOURCE_DISPATCH_ACTIONS.includes(action)) {
|
|
283
|
+
return refused("unsupported_action", `${String(action)} is outside the closed dispatch action set.`);
|
|
284
|
+
}
|
|
285
|
+
// 4. Bounded command shape. The dispatcher validates values; it never invents
|
|
286
|
+
// or rewrites them.
|
|
287
|
+
if (!isNonEmpty(command.requestFingerprint)) {
|
|
288
|
+
return refused("malformed_command", "The command needs a requestFingerprint.");
|
|
289
|
+
}
|
|
290
|
+
if (command.query !== null && !isNonEmpty(command.query)) {
|
|
291
|
+
return refused("malformed_command", "An empty query is not a query.");
|
|
292
|
+
}
|
|
293
|
+
if (command.filterDelta !== null && !isPlainRecord(command.filterDelta)) {
|
|
294
|
+
return refused("malformed_command", "filterDelta must be an object or null.");
|
|
295
|
+
}
|
|
296
|
+
// 5. A live source expansion holds the campaign. A terminal history does not.
|
|
297
|
+
if (input.activeJob !== null && input.activeJob.kind === "source_expansion") {
|
|
298
|
+
return refused("live_source_job", `A ${input.activeJob.status} source expansion (${input.activeJob.id}) already holds this campaign; wait and re-observe rather than queueing a second one.`);
|
|
299
|
+
}
|
|
300
|
+
// 6. The derived negative set. Absent lineage BLOCKS a confident
|
|
301
|
+
// "nothing has been searched" claim rather than reading as an empty set.
|
|
302
|
+
const negativeSet = deriveRefillV3SearchedNegativeSet(input.lineage, {
|
|
303
|
+
campaignId: scope.campaignId,
|
|
304
|
+
});
|
|
305
|
+
if (negativeSet.provenance === REFILL_V3_NEGATIVE_SET_PROVENANCE_UNAVAILABLE) {
|
|
306
|
+
return refused("negative_set_provenance_unavailable", "Source-run lineage is absent or pruned, so no prior-search claim can be trusted. Restore provenance before broadening or recording exhaustion.");
|
|
307
|
+
}
|
|
308
|
+
// 7. Optimistic concurrency against canonical lineage.
|
|
309
|
+
const latestForCampaign = negativeSet.entries[negativeSet.entries.length - 1];
|
|
310
|
+
if (latestForCampaign &&
|
|
311
|
+
latestForCampaign.sourceFingerprint !== scope.priorSourceFingerprint) {
|
|
312
|
+
return refused("stale_prior_fingerprint", `Canonical lineage is at ${latestForCampaign.sourceFingerprint}; the command carried ${scope.priorSourceFingerprint}.`);
|
|
313
|
+
}
|
|
314
|
+
const semantic = canonicalRefillV3SourceFingerprint(command);
|
|
315
|
+
const switching = command.family !== input.observedFamily;
|
|
316
|
+
const sameFamilyEntries = negativeSet.entries.filter((entry) => entry.family === command.family);
|
|
317
|
+
const semanticMatch = sameFamilyEntries.find((entry) => entry.semanticFingerprint === semantic);
|
|
318
|
+
// 8. A family switch needs BOTH the matching loaded guidance and a fingerprint
|
|
319
|
+
// absent from THAT family's negative set.
|
|
320
|
+
if (switching) {
|
|
321
|
+
if (command.mode === "new_provider_search" &&
|
|
322
|
+
!isNonEmpty(input.providerPromptPackage ?? null)) {
|
|
323
|
+
return refused("family_switch_prompt_missing", `Switching from ${input.observedFamily} to ${command.family} requires the newly selected family's active provider guidance to be loaded first.`);
|
|
324
|
+
}
|
|
325
|
+
if (semanticMatch) {
|
|
326
|
+
return refused("family_switch_negative_set", `The proposed ${command.family} search is already in that family's negative set.`, {
|
|
327
|
+
matchingFingerprint: semanticMatch.sourceFingerprint,
|
|
328
|
+
releaseCondition: semanticMatch.releaseCondition,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// 9. Replay versus incidental change. An exact prior fingerprint is a replay; a
|
|
333
|
+
// cosmetic edit of the same semantic query is not a material difference.
|
|
334
|
+
if (semanticMatch && !switching) {
|
|
335
|
+
const exact = semanticMatch.sourceFingerprint === command.requestFingerprint;
|
|
336
|
+
return refused(exact ? "negative_set_replay" : "incidental_change_only", exact
|
|
337
|
+
? `Request fingerprint ${command.requestFingerprint} was already searched for this campaign.`
|
|
338
|
+
: `The proposed command differs only incidentally from the already-searched ${semanticMatch.sourceFingerprint}; incidental change is not material change.`, {
|
|
339
|
+
matchingFingerprint: semanticMatch.sourceFingerprint,
|
|
340
|
+
releaseCondition: semanticMatch.releaseCondition,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
// 10. Posts-search sub-mechanics. The AI supplies the keywords and the picks;
|
|
344
|
+
// the batch size, the post cap, the per-post rationale, and the round
|
|
345
|
+
// budget are deterministic.
|
|
346
|
+
if (command.keywordBatch !== null) {
|
|
347
|
+
if (command.keywordBatch.length !== REFILL_V3_POSTS_KEYWORD_BATCH_SIZE) {
|
|
348
|
+
return refused("keyword_batch_size", `A posts-search round carries exactly ${REFILL_V3_POSTS_KEYWORD_BATCH_SIZE} keywords; ${command.keywordBatch.length} were supplied.`);
|
|
349
|
+
}
|
|
350
|
+
const roundsSpent = negativeSet.postsRoundsByCampaign[scope.campaignId] ?? 0;
|
|
351
|
+
if (roundsSpent >= REFILL_V3_POSTS_ROUND_BUDGET) {
|
|
352
|
+
return refused("posts_round_budget_spent", `The posts-search avenue is SPENT for this evidence window after ${roundsSpent} rounds. Other broadening classes and families may still apply.`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (command.selectedPostIds !== null) {
|
|
356
|
+
if (command.selectedPostIds.length > REFILL_V3_POSTS_MAX_SELECTED) {
|
|
357
|
+
return refused("post_cap_exceeded", `At most ${REFILL_V3_POSTS_MAX_SELECTED} candidate posts per round; ${command.selectedPostIds.length} were selected.`);
|
|
358
|
+
}
|
|
359
|
+
const explained = new Set((command.postRationales ?? []).map((entry) => entry.postId));
|
|
360
|
+
const unexplained = command.selectedPostIds.filter((postId) => !explained.has(postId));
|
|
361
|
+
if (unexplained.length > 0) {
|
|
362
|
+
return refused("post_rationale_missing", `Selected posts with no recorded expected-yield basis: ${unexplained.join(", ")}. A pick with no rationale is not auditable.`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// 11. Exhaustion. Per-family PRECONDITIONS first, then the shared predicate.
|
|
366
|
+
if (action === "record_exhaustion") {
|
|
367
|
+
if (command.family === "post_engager" &&
|
|
368
|
+
input.postsRefreshedInWindow !== true) {
|
|
369
|
+
return refused("post_engager_refresh_required", "post_engager cannot be declared dry until the sender's authored-posts refresh has run inside the current evidence window.");
|
|
370
|
+
}
|
|
371
|
+
if (!input.exhaustionEvidence) {
|
|
372
|
+
return refused("exhaustion_falsified", "An exhaustion claim must carry the bounded evidence it was formed from.");
|
|
373
|
+
}
|
|
374
|
+
const verdict = falsifyRefillV3SourceExhaustion(input.exhaustionEvidence);
|
|
375
|
+
if (!verdict.exhausted) {
|
|
376
|
+
return refused("exhaustion_falsified", `Live evidence contradicts the claim: ${verdict.falsifyingClause}. The flow continues.`);
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
kind: "recorded_exhaustion",
|
|
380
|
+
family: command.family,
|
|
381
|
+
receipt: {
|
|
382
|
+
ownerKey: REFILL_V3_SOURCE_FAMILY_TABLE[command.family].broadening,
|
|
383
|
+
requestFingerprint: command.requestFingerprint,
|
|
384
|
+
idempotencyKey: scope.idempotencyKey,
|
|
385
|
+
sourceBeforeFingerprint: scope.priorSourceFingerprint,
|
|
386
|
+
sourceAfterFingerprint: scope.priorSourceFingerprint,
|
|
387
|
+
sourceBeforeCount: 0,
|
|
388
|
+
sourceAfterCount: 0,
|
|
389
|
+
importedCount: 0,
|
|
390
|
+
effects: [],
|
|
391
|
+
replayState: "not_applicable",
|
|
392
|
+
rollback: { available: false, reason: "not_applicable" },
|
|
393
|
+
postRationales: null,
|
|
394
|
+
yieldEvidence: input.exhaustionEvidence.yieldEvidence,
|
|
395
|
+
},
|
|
396
|
+
exhausted: true,
|
|
397
|
+
reobserveRequired: true,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
// 12. EXACTLY ONE call to the exact existing owner through the constant table.
|
|
401
|
+
const table = REFILL_V3_SOURCE_FAMILY_TABLE[command.family];
|
|
402
|
+
const ownerKey = action === "continue" ? table.continuation : table.broadening;
|
|
403
|
+
const outcome = await input.owners[ownerKey]({
|
|
404
|
+
scope,
|
|
405
|
+
command,
|
|
406
|
+
providerPromptPackage: input.providerPromptPackage ?? null,
|
|
407
|
+
});
|
|
408
|
+
// 13. Provider INFRASTRUCTURE failure is never exhaustion, and nothing that
|
|
409
|
+
// never ran may register as searched.
|
|
410
|
+
if (outcome.kind === "failure") {
|
|
411
|
+
return {
|
|
412
|
+
kind: "scoped_failure",
|
|
413
|
+
family: command.family,
|
|
414
|
+
failureClass: outcome.failureClass,
|
|
415
|
+
retryable: true,
|
|
416
|
+
effectIdentity: null,
|
|
417
|
+
detail: `${ownerKey}: ${outcome.detail}`,
|
|
418
|
+
exhausted: false,
|
|
419
|
+
reobserveRequired: true,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (outcome.kind === "timeout") {
|
|
423
|
+
return {
|
|
424
|
+
kind: "scoped_failure",
|
|
425
|
+
family: command.family,
|
|
426
|
+
failureClass: "uncertain",
|
|
427
|
+
retryable: true,
|
|
428
|
+
effectIdentity: outcome.effectIdentity,
|
|
429
|
+
detail: `${ownerKey} returned no value; reconcile by effect identity before another mutation.`,
|
|
430
|
+
exhausted: false,
|
|
431
|
+
reobserveRequired: true,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
const receipt = outcome.receipt;
|
|
435
|
+
if (!isNonEmpty(receipt.sourceBeforeFingerprint) ||
|
|
436
|
+
!isNonEmpty(receipt.sourceAfterFingerprint) ||
|
|
437
|
+
!isNonEmpty(receipt.requestFingerprint) ||
|
|
438
|
+
!isNonEmpty(receipt.idempotencyKey)) {
|
|
439
|
+
return refused("receipt_incomplete", `${ownerKey} returned a receipt without complete source before/after and request evidence; uncertain evidence is never success.`);
|
|
440
|
+
}
|
|
441
|
+
// 14. The AI's recorded per-candidate verdicts feed the SHARED floor. The
|
|
442
|
+
// arithmetic, the comparison, and the routing are deterministic; there is
|
|
443
|
+
// no deterministic candidate judge.
|
|
444
|
+
const verdicts = input.candidateVerdicts ?? null;
|
|
445
|
+
const yieldVerdict = verdicts && verdicts.length > 0
|
|
446
|
+
? evaluateRefillV3DispatchYield({
|
|
447
|
+
family: command.family,
|
|
448
|
+
evaluated: verdicts.length,
|
|
449
|
+
passes: verdicts.filter((entry) => entry.verdict === "pass").length,
|
|
450
|
+
})
|
|
451
|
+
: receipt.yieldEvidence
|
|
452
|
+
? evaluateRefillV3DispatchYield(receipt.yieldEvidence)
|
|
453
|
+
: null;
|
|
454
|
+
// A round that MEETS the floor is a qualifying source: import and stop
|
|
455
|
+
// broadening, so its fingerprint is not a refusal for later rounds. Every
|
|
456
|
+
// other completed round registers so it cannot repeat unchanged.
|
|
457
|
+
const registers = yieldVerdict === null || yieldVerdict.decision !== "meets_floor";
|
|
458
|
+
const negativeSetAdditions = registers
|
|
459
|
+
? [
|
|
460
|
+
{
|
|
461
|
+
campaignId: scope.campaignId,
|
|
462
|
+
family: command.family,
|
|
463
|
+
sourceFingerprint: command.requestFingerprint,
|
|
464
|
+
semanticFingerprint: semantic,
|
|
465
|
+
keywordBatch: command.keywordBatch,
|
|
466
|
+
postIds: command.selectedPostIds,
|
|
467
|
+
releaseCondition: releaseConditionFor({
|
|
468
|
+
campaignId: scope.campaignId,
|
|
469
|
+
family: command.family,
|
|
470
|
+
provider: command.provider,
|
|
471
|
+
sourceFingerprint: command.requestFingerprint,
|
|
472
|
+
semanticFingerprint: semantic,
|
|
473
|
+
keywordBatch: command.keywordBatch,
|
|
474
|
+
postIds: command.selectedPostIds,
|
|
475
|
+
importedCount: receipt.importedCount,
|
|
476
|
+
yieldEvidence: receipt.yieldEvidence,
|
|
477
|
+
observedAt: scope.date,
|
|
478
|
+
searchCompleted: true,
|
|
479
|
+
}),
|
|
480
|
+
observedAt: scope.date,
|
|
481
|
+
},
|
|
482
|
+
]
|
|
483
|
+
: [];
|
|
484
|
+
return {
|
|
485
|
+
kind: "dispatched",
|
|
486
|
+
ownerKey,
|
|
487
|
+
// The PUBLIC family token is returned unchanged.
|
|
488
|
+
family: command.family,
|
|
489
|
+
receipt,
|
|
490
|
+
yieldVerdict,
|
|
491
|
+
negativeSetAdditions,
|
|
492
|
+
exhausted: false,
|
|
493
|
+
reobserveRequired: true,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
/** Re-exported bounds so no consumer restates them. */
|
|
497
|
+
export const REFILL_V3_SOURCE_DISPATCH_BOUNDS = Object.freeze({
|
|
498
|
+
keywordBatchSize: REFILL_V3_POSTS_KEYWORD_BATCH_SIZE,
|
|
499
|
+
maxSelectedPosts: REFILL_V3_POSTS_MAX_SELECTED,
|
|
500
|
+
postsRoundBudget: REFILL_V3_POSTS_ROUND_BUDGET,
|
|
501
|
+
minSourceYield: REFILL_V3_MIN_SOURCE_YIELD,
|
|
502
|
+
yieldSample: REFILL_V3_MIN_SOURCE_YIELD_SAMPLE,
|
|
503
|
+
});
|
|
504
|
+
/** The AI claim is ROUTED through the shared predicate, never re-tabled here. */
|
|
505
|
+
export function falsifyRefillV3SourceExhaustion(evidence) {
|
|
506
|
+
return REFILL_V3_EXHAUSTION_PREDICATE.evaluate(evidence);
|
|
507
|
+
}
|
|
508
|
+
export function evaluateRefillV3DispatchYield(evidence) {
|
|
509
|
+
return evaluateRefillV3SourceYield(evidence);
|
|
510
|
+
}
|
|
511
|
+
export function isRefillV3LeadSourceFamily(value) {
|
|
512
|
+
return (typeof value === "string" &&
|
|
513
|
+
REFILL_V3_LEAD_SOURCE_FAMILIES.includes(value));
|
|
514
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refill V3 campaign WATERFALL proposal/validation PACKAGE seam.
|
|
3
|
+
*
|
|
4
|
+
* The 144 authority split, restored: the BACKEND issues the complete eligible
|
|
5
|
+
* campaign set for one sender/lane row, the AI proposes an ORDER (up to three)
|
|
6
|
+
* with a rationale per entry and may propose activating one PAUSED,
|
|
7
|
+
* start-eligible campaign, and the BACKEND deterministically validates and pins
|
|
8
|
+
* that order. A proposal is never a campaign selector: a campaign the backend
|
|
9
|
+
* did not issue for that row can never enter the order.
|
|
10
|
+
*
|
|
11
|
+
* A rejection is TYPED and the row CONTINUES with the deterministic fallback
|
|
12
|
+
* order. A rejected proposal never halts the row.
|
|
13
|
+
*
|
|
14
|
+
* The validated order is pinned TASK-LOCALLY for that row's ephemeral FIFO
|
|
15
|
+
* queue. There is no durable waterfall storage, cursor, ledger, or database
|
|
16
|
+
* model, and `guidance` is caller-supplied additive context only.
|
|
17
|
+
*
|
|
18
|
+
* This module is dependency-free by design: it imports nothing, so it cannot
|
|
19
|
+
* drag app `src/**`, a validation library, or a sibling contract into the
|
|
20
|
+
* published package.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* The minimum campaign shape the validator needs. The world-state contract's
|
|
24
|
+
* `CampaignOption` satisfies it structurally, which keeps the waterfall seam
|
|
25
|
+
* dependency-free without declaring a second campaign vocabulary.
|
|
26
|
+
*/
|
|
27
|
+
export interface RefillV3WaterfallCampaignFact {
|
|
28
|
+
id: string;
|
|
29
|
+
/**
|
|
30
|
+
* ADDITIVE (146.5-12 §85, user ruling): every candidate entry in the AI's
|
|
31
|
+
* input packet also names its `campaignId` explicitly — the same value as
|
|
32
|
+
* `id`, under the name the PROPOSAL vocabulary uses — so a rationale, a
|
|
33
|
+
* receipt, or a bounded brief read can cite the campaign without a field
|
|
34
|
+
* rename in between. Optional for older callers; the validator keys on
|
|
35
|
+
* `id`.
|
|
36
|
+
*/
|
|
37
|
+
campaignId?: string;
|
|
38
|
+
status: string;
|
|
39
|
+
startEligible: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface RefillV3WaterfallSender {
|
|
42
|
+
id: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Row-scoped input, built only from transport campaign facts the observer
|
|
46
|
+
* already emits. `inactiveCampaigns` contains ONLY PAUSED entries with
|
|
47
|
+
* canonical `startEligible: true`, which is what makes an activation proposal
|
|
48
|
+
* checkable without a second eligibility rule.
|
|
49
|
+
*/
|
|
50
|
+
export type RefillV3WaterfallInput = {
|
|
51
|
+
sender: RefillV3WaterfallSender;
|
|
52
|
+
lane: string;
|
|
53
|
+
activeCampaigns: readonly RefillV3WaterfallCampaignFact[];
|
|
54
|
+
inactiveCampaigns: readonly RefillV3WaterfallCampaignFact[];
|
|
55
|
+
/**
|
|
56
|
+
* ADDITIVE (146.5-12 §85): how many actions this row can still take today.
|
|
57
|
+
* SKILL.md has documented this packet field since Plan 09; the type now
|
|
58
|
+
* declares it instead of relying on structural pass-through.
|
|
59
|
+
*/
|
|
60
|
+
availableSlots?: number;
|
|
61
|
+
/**
|
|
62
|
+
* ADDITIVE (146.5-12 cluster, user directive): the workspace's OWN
|
|
63
|
+
* per-family engagement aggregates — average accept/reply plus total sends
|
|
64
|
+
* per lead-source family, computed by the same engagement owner behind
|
|
65
|
+
* `evidence.engagement`. A family with no engagement facts is ABSENT (no
|
|
66
|
+
* data), never a fabricated zero. Real data leads; the SKILL's typical
|
|
67
|
+
* benchmarks fill the cold start.
|
|
68
|
+
*/
|
|
69
|
+
familyBenchmarks?: Readonly<Record<string, {
|
|
70
|
+
sends: number;
|
|
71
|
+
accepted: number;
|
|
72
|
+
replied: number;
|
|
73
|
+
acceptRate: number;
|
|
74
|
+
replyRate: number;
|
|
75
|
+
}>>;
|
|
76
|
+
guidance?: RefillV3WaterfallGuidance;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Additive, caller-supplied context. It introduces no database model and no
|
|
80
|
+
* durable waterfall storage; both remain out of scope.
|
|
81
|
+
*/
|
|
82
|
+
export type RefillV3WaterfallGuidance = {
|
|
83
|
+
priorChoices?: readonly string[];
|
|
84
|
+
experimentPrecedence?: readonly string[];
|
|
85
|
+
operatorPreferredCampaignId?: string;
|
|
86
|
+
};
|
|
87
|
+
export declare const REFILL_V3_WATERFALL_MAX_ORDER = 3;
|
|
88
|
+
export type RefillV3WaterfallOrderEntry = {
|
|
89
|
+
campaignId: string;
|
|
90
|
+
rationale: string;
|
|
91
|
+
};
|
|
92
|
+
export type RefillV3WaterfallProposal = {
|
|
93
|
+
order: RefillV3WaterfallOrderEntry[];
|
|
94
|
+
activation?: RefillV3WaterfallOrderEntry;
|
|
95
|
+
};
|
|
96
|
+
export declare const REFILL_V3_WATERFALL_REJECTIONS: readonly ["unissued_campaign_id", "activation_not_start_eligible", "activation_campaign_absent", "duplicate_campaign_id", "order_too_long", "order_empty", "malformed_output"];
|
|
97
|
+
export type RefillV3WaterfallRejection = (typeof REFILL_V3_WATERFALL_REJECTIONS)[number];
|
|
98
|
+
export type RefillV3WaterfallDecodeResult = {
|
|
99
|
+
ok: true;
|
|
100
|
+
value: RefillV3WaterfallProposal;
|
|
101
|
+
} | {
|
|
102
|
+
ok: false;
|
|
103
|
+
rejection: RefillV3WaterfallRejection;
|
|
104
|
+
detail: string;
|
|
105
|
+
};
|
|
106
|
+
export declare const REFILL_V3_WATERFALL_PROPOSAL_SCHEMA: Readonly<{
|
|
107
|
+
readonly type: "object";
|
|
108
|
+
readonly additionalProperties: false;
|
|
109
|
+
readonly required: readonly ["order"];
|
|
110
|
+
readonly properties: {
|
|
111
|
+
readonly order: {
|
|
112
|
+
readonly type: "array";
|
|
113
|
+
readonly minItems: 1;
|
|
114
|
+
readonly maxItems: 3;
|
|
115
|
+
readonly items: {
|
|
116
|
+
readonly type: "object";
|
|
117
|
+
readonly additionalProperties: false;
|
|
118
|
+
readonly required: readonly ["campaignId", "rationale"];
|
|
119
|
+
readonly properties: {
|
|
120
|
+
readonly campaignId: {
|
|
121
|
+
readonly type: "string";
|
|
122
|
+
readonly minLength: 1;
|
|
123
|
+
};
|
|
124
|
+
readonly rationale: {
|
|
125
|
+
readonly type: "string";
|
|
126
|
+
readonly minLength: 1;
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
readonly description: "ordered campaigns for this row; every id must already be issued for the row";
|
|
131
|
+
};
|
|
132
|
+
readonly activation: {
|
|
133
|
+
readonly type: "object";
|
|
134
|
+
readonly additionalProperties: false;
|
|
135
|
+
readonly required: readonly ["campaignId", "rationale"];
|
|
136
|
+
readonly properties: {
|
|
137
|
+
readonly campaignId: {
|
|
138
|
+
readonly type: "string";
|
|
139
|
+
readonly minLength: 1;
|
|
140
|
+
};
|
|
141
|
+
readonly rationale: {
|
|
142
|
+
readonly type: "string";
|
|
143
|
+
readonly minLength: 1;
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
readonly description: "at most one PAUSED, start-eligible campaign proposed for activation";
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
}>;
|
|
150
|
+
/**
|
|
151
|
+
* Sole validator for an AI-proposed waterfall order.
|
|
152
|
+
*
|
|
153
|
+
* The issued set is REQUIRED: membership in the row's issued campaigns is the
|
|
154
|
+
* 144 rule, and it cannot be decided from the model's output alone.
|
|
155
|
+
*/
|
|
156
|
+
export declare function decodeRefillV3WaterfallProposal(value: unknown, issued: RefillV3WaterfallInput): RefillV3WaterfallDecodeResult;
|