@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,181 @@
|
|
|
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
|
+
export const REFILL_V3_WATERFALL_MAX_ORDER = 3;
|
|
23
|
+
export const REFILL_V3_WATERFALL_REJECTIONS = [
|
|
24
|
+
"unissued_campaign_id",
|
|
25
|
+
"activation_not_start_eligible",
|
|
26
|
+
"activation_campaign_absent",
|
|
27
|
+
"duplicate_campaign_id",
|
|
28
|
+
"order_too_long",
|
|
29
|
+
"order_empty",
|
|
30
|
+
"malformed_output",
|
|
31
|
+
];
|
|
32
|
+
export const REFILL_V3_WATERFALL_PROPOSAL_SCHEMA = Object.freeze({
|
|
33
|
+
type: "object",
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
required: ["order"],
|
|
36
|
+
properties: {
|
|
37
|
+
order: {
|
|
38
|
+
type: "array",
|
|
39
|
+
minItems: 1,
|
|
40
|
+
maxItems: REFILL_V3_WATERFALL_MAX_ORDER,
|
|
41
|
+
items: {
|
|
42
|
+
type: "object",
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
required: ["campaignId", "rationale"],
|
|
45
|
+
properties: {
|
|
46
|
+
campaignId: { type: "string", minLength: 1 },
|
|
47
|
+
rationale: { type: "string", minLength: 1 },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
description: "ordered campaigns for this row; every id must already be issued for the row",
|
|
51
|
+
},
|
|
52
|
+
activation: {
|
|
53
|
+
type: "object",
|
|
54
|
+
additionalProperties: false,
|
|
55
|
+
required: ["campaignId", "rationale"],
|
|
56
|
+
properties: {
|
|
57
|
+
campaignId: { type: "string", minLength: 1 },
|
|
58
|
+
rationale: { type: "string", minLength: 1 },
|
|
59
|
+
},
|
|
60
|
+
description: "at most one PAUSED, start-eligible campaign proposed for activation",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
/**
|
|
65
|
+
* Sole validator for an AI-proposed waterfall order.
|
|
66
|
+
*
|
|
67
|
+
* The issued set is REQUIRED: membership in the row's issued campaigns is the
|
|
68
|
+
* 144 rule, and it cannot be decided from the model's output alone.
|
|
69
|
+
*/
|
|
70
|
+
export function decodeRefillV3WaterfallProposal(value, issued) {
|
|
71
|
+
if (!isRecord(value)) {
|
|
72
|
+
return malformed("Proposal must be an object.");
|
|
73
|
+
}
|
|
74
|
+
const unknownField = Object.keys(value).find((key) => !PROPOSAL_KEYS.has(key));
|
|
75
|
+
if (unknownField) {
|
|
76
|
+
return malformed(`Unknown proposal field: ${unknownField}`);
|
|
77
|
+
}
|
|
78
|
+
if (!Array.isArray(value.order)) {
|
|
79
|
+
return malformed("Proposal order must be an array.");
|
|
80
|
+
}
|
|
81
|
+
if (value.order.length === 0) {
|
|
82
|
+
return { ok: false, rejection: "order_empty", detail: "Order is empty." };
|
|
83
|
+
}
|
|
84
|
+
if (value.order.length > REFILL_V3_WATERFALL_MAX_ORDER) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
rejection: "order_too_long",
|
|
88
|
+
detail: `Order has ${value.order.length} entries; at most ${REFILL_V3_WATERFALL_MAX_ORDER} are allowed.`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const order = [];
|
|
92
|
+
for (const entry of value.order) {
|
|
93
|
+
const decoded = decodeEntry(entry);
|
|
94
|
+
if (decoded === null) {
|
|
95
|
+
return malformed("Every order entry needs a campaignId and a rationale.");
|
|
96
|
+
}
|
|
97
|
+
order.push(decoded);
|
|
98
|
+
}
|
|
99
|
+
let activation;
|
|
100
|
+
if (value.activation !== undefined) {
|
|
101
|
+
const decoded = decodeEntry(value.activation);
|
|
102
|
+
if (decoded === null) {
|
|
103
|
+
return malformed("An activation proposal needs exactly one campaignId and rationale.");
|
|
104
|
+
}
|
|
105
|
+
activation = decoded;
|
|
106
|
+
}
|
|
107
|
+
const activeIds = new Set(issued.activeCampaigns.map((entry) => entry.id));
|
|
108
|
+
for (const entry of order) {
|
|
109
|
+
if (!activeIds.has(entry.campaignId)) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
rejection: "unissued_campaign_id",
|
|
113
|
+
detail: `Campaign ${entry.campaignId} was not issued for this row.`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const orderedIds = new Set();
|
|
118
|
+
for (const entry of order) {
|
|
119
|
+
if (orderedIds.has(entry.campaignId)) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
rejection: "duplicate_campaign_id",
|
|
123
|
+
detail: `Campaign ${entry.campaignId} appears more than once.`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
orderedIds.add(entry.campaignId);
|
|
127
|
+
}
|
|
128
|
+
if (activation) {
|
|
129
|
+
if (orderedIds.has(activation.campaignId)) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
rejection: "duplicate_campaign_id",
|
|
133
|
+
detail: `Campaign ${activation.campaignId} is both ordered and proposed for activation.`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const candidate = issued.inactiveCampaigns.find((entry) => entry.id === activation?.campaignId);
|
|
137
|
+
if (!candidate) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
rejection: "activation_campaign_absent",
|
|
141
|
+
detail: `Campaign ${activation.campaignId} is not in the issued inactive set for this row.`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (candidate.startEligible !== true) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
rejection: "activation_not_start_eligible",
|
|
148
|
+
detail: `Campaign ${activation.campaignId} is not start eligible.`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
ok: true,
|
|
154
|
+
value: activation ? { order, activation } : { order },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const PROPOSAL_KEYS = new Set(["order", "activation"]);
|
|
158
|
+
const ENTRY_KEYS = new Set(["campaignId", "rationale"]);
|
|
159
|
+
function isRecord(value) {
|
|
160
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
161
|
+
}
|
|
162
|
+
function malformed(detail) {
|
|
163
|
+
return { ok: false, rejection: "malformed_output", detail };
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* An entry is projected FIELD BY FIELD from what the model actually named, so
|
|
167
|
+
* the validator can never invent, reorder, or substitute a campaign.
|
|
168
|
+
*/
|
|
169
|
+
function decodeEntry(value) {
|
|
170
|
+
if (!isRecord(value))
|
|
171
|
+
return null;
|
|
172
|
+
if (Object.keys(value).some((key) => !ENTRY_KEYS.has(key)))
|
|
173
|
+
return null;
|
|
174
|
+
if (typeof value.campaignId !== "string" ||
|
|
175
|
+
value.campaignId.trim().length === 0 ||
|
|
176
|
+
typeof value.rationale !== "string" ||
|
|
177
|
+
value.rationale.trim().length === 0) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
return { campaignId: value.campaignId, rationale: value.rationale };
|
|
181
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refill V3 waterfall-order RUNTIME VALIDATOR (Phase 146.5 Plan 09 T6, RV3WF-001).
|
|
3
|
+
*
|
|
4
|
+
* AI seam #1's owned runtime home. The subskill proposes an ORDER of up to three
|
|
5
|
+
* already-issued campaigns for one sender/lane row, with a rationale each, and
|
|
6
|
+
* MAY propose activating one PAUSED, start-eligible campaign. This module is the
|
|
7
|
+
* only thing standing between a model's free-text ordering and an unbounded
|
|
8
|
+
* campaign selector.
|
|
9
|
+
*
|
|
10
|
+
* It enforces the 144 rule through Plan 02's `decodeRefillV3WaterfallProposal`:
|
|
11
|
+
* the BACKEND issues the eligible set, the AI proposes an order, and the BACKEND
|
|
12
|
+
* validates and pins it. A campaign the backend did not issue can never enter.
|
|
13
|
+
*
|
|
14
|
+
* Every rejection is TYPED and NON-FATAL. A rejected proposal never halts the
|
|
15
|
+
* row, never mutates anything, and never causes the coordinator to skip the row:
|
|
16
|
+
* the run continues on the deterministic fallback order.
|
|
17
|
+
*
|
|
18
|
+
* It shares ONE failure taxonomy, receipt shape, and replay harness with the
|
|
19
|
+
* campaign-work validator (`A09-AI-SEAM-RUNTIME-HOME`, disposition D7) and does
|
|
20
|
+
* not fork a second AI-output framework.
|
|
21
|
+
*/
|
|
22
|
+
import { REFILL_V3_AI_OUTPUT_FAILURES, type RefillV3AiOutputFailure } from "./refill-v3-campaign-work.js";
|
|
23
|
+
import { decodeRefillV3WaterfallProposal, REFILL_V3_WATERFALL_REJECTIONS, type RefillV3WaterfallInput, type RefillV3WaterfallOrderEntry, type RefillV3WaterfallProposal, type RefillV3WaterfallRejection } from "./refill-v3-waterfall-contract.js";
|
|
24
|
+
/** The shared taxonomy, re-exported so consumers need one import. */
|
|
25
|
+
export { REFILL_V3_AI_OUTPUT_FAILURES };
|
|
26
|
+
export type { RefillV3AiOutputFailure };
|
|
27
|
+
export declare const REFILL_V3_WATERFALL_ACTIVATION_LIMIT_PER_ROW = 1;
|
|
28
|
+
export type RefillV3WaterfallRowScope = {
|
|
29
|
+
workspaceId: string;
|
|
30
|
+
senderId: string;
|
|
31
|
+
lane: string;
|
|
32
|
+
date: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The TASK-LOCAL pin. It is a plain in-memory value for one row's ephemeral FIFO
|
|
36
|
+
* queue: no durable waterfall storage, no cursor, no ledger, no database model.
|
|
37
|
+
*/
|
|
38
|
+
export type RefillV3WaterfallPinnedOrder = {
|
|
39
|
+
rowKey: string;
|
|
40
|
+
campaignIds: readonly string[];
|
|
41
|
+
rationales: Readonly<Record<string, string>>;
|
|
42
|
+
activation: RefillV3WaterfallOrderEntry | null;
|
|
43
|
+
source: "ai_validated" | "deterministic_fallback";
|
|
44
|
+
};
|
|
45
|
+
export type RefillV3WaterfallActivationReceipt = {
|
|
46
|
+
campaignId: string;
|
|
47
|
+
before: string;
|
|
48
|
+
after: string;
|
|
49
|
+
effectKey: string;
|
|
50
|
+
reobserveRequired: true;
|
|
51
|
+
};
|
|
52
|
+
export type RefillV3WaterfallDependencies = {
|
|
53
|
+
/** The EXISTING campaign mutation authority. Never a raw write. */
|
|
54
|
+
activateCampaign(input: {
|
|
55
|
+
workspaceId: string;
|
|
56
|
+
campaignId: string;
|
|
57
|
+
idempotencyKey: string;
|
|
58
|
+
}): Promise<{
|
|
59
|
+
before: string;
|
|
60
|
+
after: string;
|
|
61
|
+
effectKey: string;
|
|
62
|
+
}>;
|
|
63
|
+
reobserveExactSenderDates(input: {
|
|
64
|
+
workspaceId: string;
|
|
65
|
+
senderId: string;
|
|
66
|
+
date: string;
|
|
67
|
+
}): Promise<{
|
|
68
|
+
lanes: readonly string[];
|
|
69
|
+
}>;
|
|
70
|
+
};
|
|
71
|
+
export type RefillV3WaterfallCall = {
|
|
72
|
+
step: "validate" | "activate" | "reobserve";
|
|
73
|
+
detail: string;
|
|
74
|
+
};
|
|
75
|
+
export type RefillV3WaterfallOutcome = {
|
|
76
|
+
ok: true;
|
|
77
|
+
pinned: RefillV3WaterfallPinnedOrder;
|
|
78
|
+
activation: RefillV3WaterfallActivationReceipt | null;
|
|
79
|
+
callLog: readonly RefillV3WaterfallCall[];
|
|
80
|
+
} | {
|
|
81
|
+
ok: false;
|
|
82
|
+
rejection: RefillV3WaterfallRejection | RefillV3AiOutputFailure;
|
|
83
|
+
detail: string;
|
|
84
|
+
/** A rejection is NON-FATAL: the row continues on this order. */
|
|
85
|
+
fallback: RefillV3WaterfallPinnedOrder;
|
|
86
|
+
callLog: readonly RefillV3WaterfallCall[];
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* TASK-LOCAL activation ledger, bounded to one activation per row per task.
|
|
90
|
+
* Nothing durable: it exists so a second activation inside one task is
|
|
91
|
+
* impossible, not to remember anything across tasks.
|
|
92
|
+
*/
|
|
93
|
+
export type RefillV3WaterfallTaskScope = {
|
|
94
|
+
readonly activationsByRow: Map<string, number>;
|
|
95
|
+
};
|
|
96
|
+
export declare function createRefillV3WaterfallTaskScope(): RefillV3WaterfallTaskScope;
|
|
97
|
+
export declare function refillV3WaterfallRowKey(scope: RefillV3WaterfallRowScope): string;
|
|
98
|
+
/**
|
|
99
|
+
* The deterministic FALLBACK order.
|
|
100
|
+
*
|
|
101
|
+
* It is the backend's own canonical issue order for the row, consumed verbatim.
|
|
102
|
+
* Deriving a second ordering rule here would put ordering policy in two places.
|
|
103
|
+
*/
|
|
104
|
+
export declare function refillV3FallbackWaterfallOrder(issued: RefillV3WaterfallInput, scope: RefillV3WaterfallRowScope): RefillV3WaterfallPinnedOrder;
|
|
105
|
+
/**
|
|
106
|
+
* Validation is MEMBERSHIP, START-ELIGIBILITY, and SHAPE only.
|
|
107
|
+
*
|
|
108
|
+
* It deliberately holds no ordering policy: the model keeps the judgment, and a
|
|
109
|
+
* validator that re-sorted the proposal would silently replace that judgment
|
|
110
|
+
* with a second, undocumented ordering rule.
|
|
111
|
+
*/
|
|
112
|
+
export declare function validateRefillV3WaterfallProposal(value: unknown, issued: RefillV3WaterfallInput): {
|
|
113
|
+
ok: true;
|
|
114
|
+
value: RefillV3WaterfallProposal;
|
|
115
|
+
} | {
|
|
116
|
+
ok: false;
|
|
117
|
+
rejection: RefillV3WaterfallRejection;
|
|
118
|
+
detail: string;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Validate ONE proposal, execute at most one activation through the EXISTING
|
|
122
|
+
* authority, and force the immediate two-lane re-observation.
|
|
123
|
+
*
|
|
124
|
+
* A rejection is a VALUE carrying the deterministic fallback: the row continues,
|
|
125
|
+
* and the caller never has to decide what a thrown validation error means.
|
|
126
|
+
*/
|
|
127
|
+
export declare function applyRefillV3WaterfallProposal(input: {
|
|
128
|
+
value: unknown;
|
|
129
|
+
issued: RefillV3WaterfallInput;
|
|
130
|
+
scope: RefillV3WaterfallRowScope;
|
|
131
|
+
dependencies: RefillV3WaterfallDependencies;
|
|
132
|
+
taskScope?: RefillV3WaterfallTaskScope;
|
|
133
|
+
idempotencyKey?: string;
|
|
134
|
+
}): Promise<RefillV3WaterfallOutcome>;
|
|
135
|
+
/** Re-exported so no consumer restates the closed rejection vocabulary. */
|
|
136
|
+
export { decodeRefillV3WaterfallProposal, REFILL_V3_WATERFALL_REJECTIONS };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refill V3 waterfall-order RUNTIME VALIDATOR (Phase 146.5 Plan 09 T6, RV3WF-001).
|
|
3
|
+
*
|
|
4
|
+
* AI seam #1's owned runtime home. The subskill proposes an ORDER of up to three
|
|
5
|
+
* already-issued campaigns for one sender/lane row, with a rationale each, and
|
|
6
|
+
* MAY propose activating one PAUSED, start-eligible campaign. This module is the
|
|
7
|
+
* only thing standing between a model's free-text ordering and an unbounded
|
|
8
|
+
* campaign selector.
|
|
9
|
+
*
|
|
10
|
+
* It enforces the 144 rule through Plan 02's `decodeRefillV3WaterfallProposal`:
|
|
11
|
+
* the BACKEND issues the eligible set, the AI proposes an order, and the BACKEND
|
|
12
|
+
* validates and pins it. A campaign the backend did not issue can never enter.
|
|
13
|
+
*
|
|
14
|
+
* Every rejection is TYPED and NON-FATAL. A rejected proposal never halts the
|
|
15
|
+
* row, never mutates anything, and never causes the coordinator to skip the row:
|
|
16
|
+
* the run continues on the deterministic fallback order.
|
|
17
|
+
*
|
|
18
|
+
* It shares ONE failure taxonomy, receipt shape, and replay harness with the
|
|
19
|
+
* campaign-work validator (`A09-AI-SEAM-RUNTIME-HOME`, disposition D7) and does
|
|
20
|
+
* not fork a second AI-output framework.
|
|
21
|
+
*/
|
|
22
|
+
import { REFILL_V3_AI_OUTPUT_FAILURES, } from "./refill-v3-campaign-work.js";
|
|
23
|
+
import { decodeRefillV3WaterfallProposal, REFILL_V3_WATERFALL_MAX_ORDER, REFILL_V3_WATERFALL_REJECTIONS, } from "./refill-v3-waterfall-contract.js";
|
|
24
|
+
/** The shared taxonomy, re-exported so consumers need one import. */
|
|
25
|
+
export { REFILL_V3_AI_OUTPUT_FAILURES };
|
|
26
|
+
export const REFILL_V3_WATERFALL_ACTIVATION_LIMIT_PER_ROW = 1;
|
|
27
|
+
export function createRefillV3WaterfallTaskScope() {
|
|
28
|
+
return { activationsByRow: new Map() };
|
|
29
|
+
}
|
|
30
|
+
export function refillV3WaterfallRowKey(scope) {
|
|
31
|
+
return `${scope.workspaceId}:${scope.senderId}:${scope.lane}:${scope.date}`;
|
|
32
|
+
}
|
|
33
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
34
|
+
// STUB — behaviour lands at this task's GREEN commit.
|
|
35
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* The deterministic FALLBACK order.
|
|
38
|
+
*
|
|
39
|
+
* It is the backend's own canonical issue order for the row, consumed verbatim.
|
|
40
|
+
* Deriving a second ordering rule here would put ordering policy in two places.
|
|
41
|
+
*/
|
|
42
|
+
export function refillV3FallbackWaterfallOrder(issued, scope) {
|
|
43
|
+
return {
|
|
44
|
+
rowKey: refillV3WaterfallRowKey(scope),
|
|
45
|
+
campaignIds: issued.activeCampaigns
|
|
46
|
+
.slice(0, REFILL_V3_WATERFALL_MAX_ORDER)
|
|
47
|
+
.map((entry) => entry.id),
|
|
48
|
+
rationales: {},
|
|
49
|
+
activation: null,
|
|
50
|
+
source: "deterministic_fallback",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Validation is MEMBERSHIP, START-ELIGIBILITY, and SHAPE only.
|
|
55
|
+
*
|
|
56
|
+
* It deliberately holds no ordering policy: the model keeps the judgment, and a
|
|
57
|
+
* validator that re-sorted the proposal would silently replace that judgment
|
|
58
|
+
* with a second, undocumented ordering rule.
|
|
59
|
+
*/
|
|
60
|
+
export function validateRefillV3WaterfallProposal(value, issued) {
|
|
61
|
+
const decoded = decodeRefillV3WaterfallProposal(value, issued);
|
|
62
|
+
return decoded.ok
|
|
63
|
+
? { ok: true, value: decoded.value }
|
|
64
|
+
: { ok: false, rejection: decoded.rejection, detail: decoded.detail };
|
|
65
|
+
}
|
|
66
|
+
function pinnedFrom(proposal, scope) {
|
|
67
|
+
const rationales = {};
|
|
68
|
+
for (const entry of proposal.order) {
|
|
69
|
+
rationales[entry.campaignId] = entry.rationale;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
rowKey: refillV3WaterfallRowKey(scope),
|
|
73
|
+
// The model's exact order, projected entry by entry.
|
|
74
|
+
campaignIds: proposal.order.map((entry) => entry.campaignId),
|
|
75
|
+
rationales,
|
|
76
|
+
activation: proposal.activation ?? null,
|
|
77
|
+
source: "ai_validated",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Validate ONE proposal, execute at most one activation through the EXISTING
|
|
82
|
+
* authority, and force the immediate two-lane re-observation.
|
|
83
|
+
*
|
|
84
|
+
* A rejection is a VALUE carrying the deterministic fallback: the row continues,
|
|
85
|
+
* and the caller never has to decide what a thrown validation error means.
|
|
86
|
+
*/
|
|
87
|
+
export async function applyRefillV3WaterfallProposal(input) {
|
|
88
|
+
const { issued, scope, dependencies, taskScope } = input;
|
|
89
|
+
const callLog = [];
|
|
90
|
+
const fallback = refillV3FallbackWaterfallOrder(issued, scope);
|
|
91
|
+
const rowKey = refillV3WaterfallRowKey(scope);
|
|
92
|
+
const validated = validateRefillV3WaterfallProposal(input.value, issued);
|
|
93
|
+
callLog.push({ step: "validate", detail: rowKey });
|
|
94
|
+
if (!validated.ok) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
rejection: validated.rejection,
|
|
98
|
+
detail: validated.detail,
|
|
99
|
+
fallback,
|
|
100
|
+
callLog,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const pinned = pinnedFrom(validated.value, scope);
|
|
104
|
+
const activation = validated.value.activation ?? null;
|
|
105
|
+
if (!activation) {
|
|
106
|
+
return { ok: true, pinned, activation: null, callLog };
|
|
107
|
+
}
|
|
108
|
+
// At most ONE activation per row per task. The bound is task-local and exists
|
|
109
|
+
// so a second activation inside one task is impossible.
|
|
110
|
+
const used = taskScope?.activationsByRow.get(rowKey) ?? 0;
|
|
111
|
+
if (used >= REFILL_V3_WATERFALL_ACTIVATION_LIMIT_PER_ROW) {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
rejection: "activation_campaign_absent",
|
|
115
|
+
detail: `This row already used its ${REFILL_V3_WATERFALL_ACTIVATION_LIMIT_PER_ROW} activation in this task.`,
|
|
116
|
+
fallback,
|
|
117
|
+
callLog,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
let receipt;
|
|
121
|
+
try {
|
|
122
|
+
const effect = await dependencies.activateCampaign({
|
|
123
|
+
workspaceId: scope.workspaceId,
|
|
124
|
+
campaignId: activation.campaignId,
|
|
125
|
+
idempotencyKey: input.idempotencyKey ?? `waterfall:${rowKey}`,
|
|
126
|
+
});
|
|
127
|
+
callLog.push({ step: "activate", detail: activation.campaignId });
|
|
128
|
+
receipt = {
|
|
129
|
+
campaignId: activation.campaignId,
|
|
130
|
+
before: effect.before,
|
|
131
|
+
after: effect.after,
|
|
132
|
+
effectKey: effect.effectKey,
|
|
133
|
+
reobserveRequired: true,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
// An authority failure is typed and NON-FATAL: the row continues on the
|
|
138
|
+
// fallback rather than claiming an activation nobody observed.
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
rejection: "activation_campaign_absent",
|
|
142
|
+
detail: `The existing campaign authority refused or failed for ${activation.campaignId}: ${error instanceof Error ? error.name : "unknown"}`,
|
|
143
|
+
fallback,
|
|
144
|
+
callLog,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
taskScope?.activationsByRow.set(rowKey, used + 1);
|
|
148
|
+
// Immediate two-lane re-observation BEFORE any further decision.
|
|
149
|
+
await dependencies.reobserveExactSenderDates({
|
|
150
|
+
workspaceId: scope.workspaceId,
|
|
151
|
+
senderId: scope.senderId,
|
|
152
|
+
date: scope.date,
|
|
153
|
+
});
|
|
154
|
+
callLog.push({
|
|
155
|
+
step: "reobserve",
|
|
156
|
+
detail: `${scope.senderId}:${scope.date}`,
|
|
157
|
+
});
|
|
158
|
+
return { ok: true, pinned, activation: receipt, callLog };
|
|
159
|
+
}
|
|
160
|
+
/** Re-exported so no consumer restates the closed rejection vocabulary. */
|
|
161
|
+
export { decodeRefillV3WaterfallProposal, REFILL_V3_WATERFALL_REJECTIONS };
|