@mj-biz-apps/marketing-server 0.3.0 → 0.5.0
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/__tests__/adConversionParity.test.d.ts +2 -0
- package/dist/__tests__/adConversionParity.test.d.ts.map +1 -0
- package/dist/__tests__/adConversionParity.test.js +127 -0
- package/dist/__tests__/adConversionParity.test.js.map +1 -0
- package/dist/__tests__/googleCreativeImage.test.d.ts +2 -0
- package/dist/__tests__/googleCreativeImage.test.d.ts.map +1 -0
- package/dist/__tests__/googleCreativeImage.test.js +53 -0
- package/dist/__tests__/googleCreativeImage.test.js.map +1 -0
- package/dist/__tests__/linkedInTargeting.test.d.ts +2 -0
- package/dist/__tests__/linkedInTargeting.test.d.ts.map +1 -0
- package/dist/__tests__/linkedInTargeting.test.js +65 -0
- package/dist/__tests__/linkedInTargeting.test.js.map +1 -0
- package/dist/agents/MarketingCreativeAgent.d.ts +16 -0
- package/dist/agents/MarketingCreativeAgent.d.ts.map +1 -0
- package/dist/agents/MarketingCreativeAgent.js +28 -0
- package/dist/agents/MarketingCreativeAgent.js.map +1 -0
- package/dist/agents/MarketingExperimentationAgent.d.ts +39 -0
- package/dist/agents/MarketingExperimentationAgent.d.ts.map +1 -1
- package/dist/agents/MarketingExperimentationAgent.js +73 -3
- package/dist/agents/MarketingExperimentationAgent.js.map +1 -1
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +3 -0
- package/dist/agents/index.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/programs/google/GoogleAdsClient.d.ts +38 -3
- package/dist/programs/google/GoogleAdsClient.d.ts.map +1 -1
- package/dist/programs/google/GoogleAdsClient.js +96 -14
- package/dist/programs/google/GoogleAdsClient.js.map +1 -1
- package/dist/programs/google/GoogleAdsProgramDriver.d.ts +19 -1
- package/dist/programs/google/GoogleAdsProgramDriver.d.ts.map +1 -1
- package/dist/programs/google/GoogleAdsProgramDriver.js +165 -14
- package/dist/programs/google/GoogleAdsProgramDriver.js.map +1 -1
- package/dist/programs/google/googleBrandAssets.d.ts +9 -0
- package/dist/programs/google/googleBrandAssets.d.ts.map +1 -1
- package/dist/programs/google/googleBrandAssets.js +30 -16
- package/dist/programs/google/googleBrandAssets.js.map +1 -1
- package/dist/programs/index.d.ts +4 -0
- package/dist/programs/index.d.ts.map +1 -1
- package/dist/programs/index.js +5 -0
- package/dist/programs/index.js.map +1 -1
- package/dist/programs/linkedin/LinkedInAdsClient.d.ts +123 -2
- package/dist/programs/linkedin/LinkedInAdsClient.d.ts.map +1 -1
- package/dist/programs/linkedin/LinkedInAdsClient.js +183 -26
- package/dist/programs/linkedin/LinkedInAdsClient.js.map +1 -1
- package/dist/programs/linkedin/LinkedInAdsProgramDriver.d.ts +38 -7
- package/dist/programs/linkedin/LinkedInAdsProgramDriver.d.ts.map +1 -1
- package/dist/programs/linkedin/LinkedInAdsProgramDriver.js +365 -70
- package/dist/programs/linkedin/LinkedInAdsProgramDriver.js.map +1 -1
- package/dist/programs/linkedin/linkedInExecutionContext.d.ts +6 -0
- package/dist/programs/linkedin/linkedInExecutionContext.d.ts.map +1 -1
- package/dist/programs/linkedin/linkedInExecutionContext.js +2 -1
- package/dist/programs/linkedin/linkedInExecutionContext.js.map +1 -1
- package/dist/programs/meta/MetaAdsClient.d.ts +134 -0
- package/dist/programs/meta/MetaAdsClient.d.ts.map +1 -0
- package/dist/programs/meta/MetaAdsClient.js +280 -0
- package/dist/programs/meta/MetaAdsClient.js.map +1 -0
- package/dist/programs/meta/MetaAdsProgramDriver.d.ts +88 -0
- package/dist/programs/meta/MetaAdsProgramDriver.d.ts.map +1 -0
- package/dist/programs/meta/MetaAdsProgramDriver.js +536 -0
- package/dist/programs/meta/MetaAdsProgramDriver.js.map +1 -0
- package/dist/programs/meta/metaAdsExecutionContext.d.ts +39 -0
- package/dist/programs/meta/metaAdsExecutionContext.d.ts.map +1 -0
- package/dist/programs/meta/metaAdsExecutionContext.js +99 -0
- package/dist/programs/meta/metaAdsExecutionContext.js.map +1 -0
- package/package.json +9 -8
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adConversionParity.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/adConversionParity.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { GoogleAdsClient } from '../programs/google/GoogleAdsClient.js';
|
|
3
|
+
import { MetaAdsClient, customConversionActionType, metaObjectiveFor } from '../programs/meta/MetaAdsClient.js';
|
|
4
|
+
function googleClient(answer) {
|
|
5
|
+
const calls = [];
|
|
6
|
+
const fetchImpl = (async (input, init) => {
|
|
7
|
+
const url = String(input);
|
|
8
|
+
calls.push({ url, init });
|
|
9
|
+
if (url.endsWith('/token'))
|
|
10
|
+
return new Response(JSON.stringify({ access_token: 't', expires_in: 3600 }), { status: 200 });
|
|
11
|
+
const body = init?.body ? JSON.parse(String(init.body)) : null;
|
|
12
|
+
return new Response(JSON.stringify(answer(url, body)), { status: 200 });
|
|
13
|
+
});
|
|
14
|
+
return { client: new GoogleAdsClient({ clientId: 'c', clientSecret: 's', refreshToken: 'r', customerId: '4684433735', fetchImpl }), calls };
|
|
15
|
+
}
|
|
16
|
+
const queryOf = (c) => JSON.parse(String(c.init?.body)).query ?? '';
|
|
17
|
+
describe('GoogleAdsClient — conversion actions (D-34 parity)', () => {
|
|
18
|
+
it('lists the customer\'s conversion actions by id, with whether the account counts them by default', async () => {
|
|
19
|
+
const { client, calls } = googleClient(() => [{ results: [
|
|
20
|
+
{ conversionAction: { resourceName: 'customers/4684433735/conversionActions/7845520214', id: '7845520214', name: 'Ascend Book Download - Google - Form Submission', status: 'ENABLED', category: 'SUBMIT_LEAD_FORM', origin: 'WEBSITE', primaryForGoal: false, includeInConversionsMetric: false } },
|
|
21
|
+
{ conversionAction: { resourceName: 'customers/4684433735/conversionActions/7217103107', id: '7217103107', name: 'SC | GA4 | Purchase', status: 'ENABLED', category: 'PURCHASE', origin: 'WEBSITE', primaryForGoal: true, includeInConversionsMetric: true } },
|
|
22
|
+
{ conversionAction: { resourceName: 'customers/4684433735/conversionActions/1', id: '1', name: 'Paused one', status: 'PAUSED', primaryForGoal: true } },
|
|
23
|
+
] }]);
|
|
24
|
+
const actions = await client.conversionActions();
|
|
25
|
+
expect(actions.map(a => [a.id, a.enabled, a.countsByDefault])).toEqual([['7845520214', true, false], ['7217103107', true, true], ['1', false, true]]);
|
|
26
|
+
expect(queryOf(calls[1])).toContain("FROM conversion_action WHERE conversion_action.status != 'REMOVED'");
|
|
27
|
+
expect(await client.findConversionActionById(' 7845520214 ')).toMatchObject({ name: 'Ascend Book Download - Google - Form Submission' });
|
|
28
|
+
expect(await client.findConversionActionById('999')).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
it('creates a one-action custom goal named after the action and points the campaign at it with a CAMPAIGN-level goal config', async () => {
|
|
31
|
+
const { client, calls } = googleClient((url) => url.includes('customConversionGoals') ? { results: [{ resourceName: 'customers/4684433735/customConversionGoals/55' }] } : url.includes('searchStream') ? [] : { results: [{ resourceName: 'x' }] });
|
|
32
|
+
const goal = await client.ensureCustomConversionGoal('customers/4684433735/conversionActions/7845520214', 'Marketing: Ascend Book Download - Google - Form Submission');
|
|
33
|
+
expect(goal).toBe('customers/4684433735/customConversionGoals/55');
|
|
34
|
+
const lookup = calls.find(c => c.url.includes('searchStream'));
|
|
35
|
+
expect(queryOf(lookup)).toContain('FROM custom_conversion_goal');
|
|
36
|
+
const create = calls.find(c => c.url.includes('customConversionGoals:mutate'));
|
|
37
|
+
expect(JSON.parse(String(create.init?.body))).toEqual({ operations: [{ create: { name: 'Marketing: Ascend Book Download - Google - Form Submission', conversionActions: ['customers/4684433735/conversionActions/7845520214'], status: 'ENABLED' } }] });
|
|
38
|
+
await client.setCampaignConversionGoal('customers/4684433735/campaigns/24313683017', goal);
|
|
39
|
+
const point = calls.find(c => c.url.includes('conversionGoalCampaignConfigs:mutate'));
|
|
40
|
+
expect(JSON.parse(String(point.init?.body))).toEqual({ operations: [{
|
|
41
|
+
update: { resourceName: 'customers/4684433735/conversionGoalCampaignConfigs/24313683017', goalConfigLevel: 'CAMPAIGN', customConversionGoal: goal },
|
|
42
|
+
updateMask: 'goal_config_level,custom_conversion_goal',
|
|
43
|
+
}] });
|
|
44
|
+
// Google allows ONE goal per action list on the customer: an existing goal holding exactly this action is reused
|
|
45
|
+
// as is (whoever made it, whatever its name), a goal holding more actions is not a match.
|
|
46
|
+
const { client: adoptingClient, calls: adoptCalls } = googleClient(() => [{ results: [
|
|
47
|
+
{ customConversionGoal: { resourceName: 'customers/4684433735/customConversionGoals/9', name: 'Agency bundle', conversionActions: ['customers/4684433735/conversionActions/7845520214', 'customers/4684433735/conversionActions/7217103107'] } },
|
|
48
|
+
{ customConversionGoal: { resourceName: 'customers/4684433735/customConversionGoals/6459699002', name: 'CG-01 — conversion goal', conversionActions: ['customers/4684433735/conversionActions/7845520214'] } },
|
|
49
|
+
] }]);
|
|
50
|
+
expect(await adoptingClient.ensureCustomConversionGoal('customers/4684433735/conversionActions/7845520214', 'Marketing: whatever')).toBe('customers/4684433735/customConversionGoals/6459699002');
|
|
51
|
+
expect(adoptCalls.some(c => c.url.includes('customConversionGoals:mutate'))).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
it('measures the named action alone (all_conversions segmented by it) and leaves impressions, clicks and cost unsegmented', async () => {
|
|
54
|
+
const { client, calls } = googleClient((_url, body) => {
|
|
55
|
+
const q = body.query;
|
|
56
|
+
return q.includes('segments.conversion_action')
|
|
57
|
+
? [{ results: [{ metrics: { allConversions: 3 } }] }]
|
|
58
|
+
: [{ results: [{ metrics: { impressions: 1000, clicks: 40, conversions: 0, costMicros: 12_500_000 } }] }];
|
|
59
|
+
});
|
|
60
|
+
const m = await client.campaignMetrics(['customers/4684433735/campaigns/1'], new Date('2026-10-01T00:00:00Z'), new Date('2026-10-14T00:00:00Z'), 'customers/4684433735/conversionActions/7845520214');
|
|
61
|
+
expect(m).toEqual({ impressions: 1000, clicks: 40, conversions: 3, spendUsd: 12.5 });
|
|
62
|
+
const queries = calls.filter(c => c.url.includes('searchStream')).map(queryOf);
|
|
63
|
+
expect(queries).toHaveLength(2);
|
|
64
|
+
expect(queries[0]).not.toContain('segments.conversion_action');
|
|
65
|
+
expect(queries[1]).toContain("SELECT metrics.all_conversions FROM campaign WHERE campaign.resource_name IN ('customers/4684433735/campaigns/1') AND segments.date BETWEEN '2026-10-01' AND '2026-10-14' AND segments.conversion_action = 'customers/4684433735/conversionActions/7845520214'");
|
|
66
|
+
const plain = await client.campaignMetrics(['customers/4684433735/campaigns/1'], new Date('2026-10-01T00:00:00Z'), new Date('2026-10-14T00:00:00Z'));
|
|
67
|
+
expect(plain.conversions).toBe(0);
|
|
68
|
+
expect(calls.filter(c => c.url.includes('searchStream'))).toHaveLength(3);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
function metaClient(answer) {
|
|
72
|
+
const calls = [];
|
|
73
|
+
const fetchImpl = (async (input, init) => {
|
|
74
|
+
const url = new URL(String(input));
|
|
75
|
+
const body = init?.body instanceof URLSearchParams ? init.body : null;
|
|
76
|
+
calls.push({ url, body });
|
|
77
|
+
return new Response(JSON.stringify(answer(url, body)), { status: 200 });
|
|
78
|
+
});
|
|
79
|
+
return { client: new MetaAdsClient({ appSecret: 's', accessToken: 't', adAccountId: 'act_2312673322359408', fetchImpl }), calls };
|
|
80
|
+
}
|
|
81
|
+
describe('MetaAdsClient — custom conversions (D-34 parity)', () => {
|
|
82
|
+
it('lists custom conversions by id with archived ones disabled, and finds one by id', async () => {
|
|
83
|
+
const { client, calls } = metaClient(() => ({ data: [
|
|
84
|
+
{ id: '795683087750734', name: 'SC | Ascend Download', custom_event_type: 'LEAD', is_archived: false, pixel: { id: '415938279713144' } },
|
|
85
|
+
{ id: '167817987988570', name: 'Newsletter Subscribe', custom_event_type: 'SUBSCRIBE', is_archived: true, pixel: { id: '415938279713144' } },
|
|
86
|
+
] }));
|
|
87
|
+
const list = await client.customConversions();
|
|
88
|
+
expect(list).toEqual([
|
|
89
|
+
{ id: '795683087750734', name: 'SC | Ascend Download', enabled: true, customEventType: 'LEAD', pixelId: '415938279713144' },
|
|
90
|
+
{ id: '167817987988570', name: 'Newsletter Subscribe', enabled: false, customEventType: 'SUBSCRIBE', pixelId: '415938279713144' },
|
|
91
|
+
]);
|
|
92
|
+
expect(calls[0].url.pathname).toBe('/v21.0/act_2312673322359408/customconversions');
|
|
93
|
+
expect(calls[0].url.searchParams.get('fields')).toBe('id,name,custom_event_type,is_archived,pixel{id}');
|
|
94
|
+
expect(await client.findCustomConversionById('795683087750734')).toMatchObject({ name: 'SC | Ascend Download' });
|
|
95
|
+
});
|
|
96
|
+
it('creates the ad set optimising for the custom conversion (custom_conversion_id only — Meta 400s 100/1885014 if pixel_id is included), and the campaign under the objective the conversion calls for', async () => {
|
|
97
|
+
const { client, calls } = metaClient(() => ({ id: '120' }));
|
|
98
|
+
await client.createCampaign({ name: 'CX-05 — campaign', status: 'PAUSED', objective: 'OUTCOME_LEADS' });
|
|
99
|
+
expect(calls[0].body?.get('objective')).toBe('OUTCOME_LEADS');
|
|
100
|
+
await client.createAdSet({ name: 'CX-05 — ad set', campaignId: '120', dailyBudgetUsd: 25, status: 'PAUSED', runSchedule: { start: 1_792_000_000_000 }, countries: ['US'], conversion: { customConversionId: '795683087750734', pixelId: '415938279713144' } });
|
|
101
|
+
const adSet = calls[1].body;
|
|
102
|
+
expect(adSet.get('optimization_goal')).toBe('OFFSITE_CONVERSIONS');
|
|
103
|
+
expect(JSON.parse(adSet.get('promoted_object'))).toEqual({ custom_conversion_id: '795683087750734' });
|
|
104
|
+
await client.createAdSet({ name: 'CX-06 — ad set', campaignId: '120', dailyBudgetUsd: 25, status: 'PAUSED', runSchedule: { start: 1_792_000_000_000 }, countries: ['US'] });
|
|
105
|
+
expect(calls[2].body?.get('optimization_goal')).toBe('LINK_CLICKS');
|
|
106
|
+
expect(calls[2].body?.has('promoted_object')).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
it('counts only the named conversion\'s action when one is given, every offsite/lead action otherwise', async () => {
|
|
109
|
+
const rows = { data: [{ impressions: '500', clicks: '20', spend: '10.5', actions: [
|
|
110
|
+
{ action_type: 'offsite_conversion.custom.795683087750734', value: '2' },
|
|
111
|
+
{ action_type: 'offsite_conversion.fb_pixel_lead', value: '5' },
|
|
112
|
+
{ action_type: 'link_click', value: '20' },
|
|
113
|
+
] }] };
|
|
114
|
+
const { client } = metaClient(() => rows);
|
|
115
|
+
expect((await client.adSetMetrics(['1'], new Date('2026-10-01T00:00:00Z'), new Date('2026-10-14T00:00:00Z'), '795683087750734')).conversions).toBe(2);
|
|
116
|
+
expect((await client.adSetMetrics(['1'], new Date('2026-10-01T00:00:00Z'), new Date('2026-10-14T00:00:00Z'))).conversions).toBe(7);
|
|
117
|
+
expect(customConversionActionType('795683087750734')).toBe('offsite_conversion.custom.795683087750734');
|
|
118
|
+
});
|
|
119
|
+
it('picks the ODAX objective from the conversion\'s event class', () => {
|
|
120
|
+
expect(metaObjectiveFor(null)).toBe('OUTCOME_TRAFFIC');
|
|
121
|
+
expect(metaObjectiveFor({ id: '1', name: 'x', enabled: true, customEventType: 'LEAD', pixelId: null })).toBe('OUTCOME_LEADS');
|
|
122
|
+
expect(metaObjectiveFor({ id: '1', name: 'x', enabled: true, customEventType: 'complete_registration', pixelId: null })).toBe('OUTCOME_LEADS');
|
|
123
|
+
expect(metaObjectiveFor({ id: '1', name: 'x', enabled: true, customEventType: 'PURCHASE', pixelId: null })).toBe('OUTCOME_SALES');
|
|
124
|
+
expect(metaObjectiveFor({ id: '1', name: 'x', enabled: true, customEventType: 'OTHER', pixelId: null })).toBe('OUTCOME_SALES');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
//# sourceMappingURL=adConversionParity.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adConversionParity.test.js","sourceRoot":"","sources":["../../src/__tests__/adConversionParity.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"googleCreativeImage.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/googleCreativeImage.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { GoogleAdsProgramDriver } from '../programs/google/GoogleAdsProgramDriver.js';
|
|
3
|
+
/** A PNG whose IHDR says width×height — enough for the header-only dimension reader. */
|
|
4
|
+
function png(width, height) {
|
|
5
|
+
const b = new Uint8Array(33);
|
|
6
|
+
b.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], 0);
|
|
7
|
+
const dv = new DataView(b.buffer);
|
|
8
|
+
dv.setUint32(8, 13);
|
|
9
|
+
b.set([0x49, 0x48, 0x44, 0x52], 12);
|
|
10
|
+
dv.setUint32(16, width);
|
|
11
|
+
dv.setUint32(20, height);
|
|
12
|
+
return b;
|
|
13
|
+
}
|
|
14
|
+
const user = { ID: 'U1', Email: 'harness@example.test' };
|
|
15
|
+
const ctx = async () => ({ ok: true, client: { conversionActions: async () => [], findConversionActionById: async () => null }, brandConnectionId: 'C1', customerId: '4684433735', brandAssets: null, launchState: 'Paused' });
|
|
16
|
+
const program = (levers) => ({ id: 'P1', brandId: 'B1', params: { tacticConfigJSON: JSON.stringify({ audience: 'targeted', dailyBudgetUsd: 25, landingPageUrl: 'https://sidecar.ai/ai', optimizeFor: 'clicks', creativeHeadlines: ['Free Ebook'], creativeDescriptions: ['Download it'], ...levers }) } });
|
|
17
|
+
describe('GoogleAdsProgramDriver — the program image is held to Google\'s landscape slot at propose (KI-156)', () => {
|
|
18
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
19
|
+
it('refuses a 16:9 generator frame with GA_CREATIVE_IMAGE_RATIO naming the size', async () => {
|
|
20
|
+
vi.stubGlobal('fetch', async () => new Response(Buffer.from(png(1376, 768)), { status: 200, headers: { 'content-type': 'image/png' } }));
|
|
21
|
+
const issues = await new GoogleAdsProgramDriver(user, ctx).validateProposal(program({ creativeImageUrl: 'https://example.test/ad.png' }));
|
|
22
|
+
const hit = issues.find(i => i.code === 'GA_CREATIVE_IMAGE_RATIO');
|
|
23
|
+
expect(hit?.severity).toBe('error');
|
|
24
|
+
expect(hit?.message).toMatch(/1376×768/);
|
|
25
|
+
expect(hit?.message).toMatch(/1\.91:1/);
|
|
26
|
+
});
|
|
27
|
+
it('accepts a 1.91:1 image and says nothing', async () => {
|
|
28
|
+
vi.stubGlobal('fetch', async () => new Response(Buffer.from(png(1200, 628)), { status: 200, headers: { 'content-type': 'image/png' } }));
|
|
29
|
+
const issues = await new GoogleAdsProgramDriver(user, ctx).validateProposal(program({ creativeImageUrl: 'https://example.test/ad.png' }));
|
|
30
|
+
expect(issues.filter(i => i.code.startsWith('GA_CREATIVE_IMAGE'))).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
it('warns, never refuses, when the image cannot be fetched', async () => {
|
|
33
|
+
vi.stubGlobal('fetch', async () => { throw new Error('ECONNRESET'); });
|
|
34
|
+
const issues = await new GoogleAdsProgramDriver(user, ctx).validateProposal(program({ creativeImageUrl: 'https://example.test/ad.png' }));
|
|
35
|
+
const hit = issues.find(i => i.code === 'GA_CREATIVE_IMAGE_UNREACHABLE');
|
|
36
|
+
expect(hit?.severity).toBe('warning');
|
|
37
|
+
expect(hit?.message).not.toMatch(/fall back/);
|
|
38
|
+
});
|
|
39
|
+
it('reports GA_CREATIVE_IMAGE_INVALID when the URL serves a non-image content-type', async () => {
|
|
40
|
+
vi.stubGlobal('fetch', async () => new Response('<html>not an image</html>', { status: 200, headers: { 'content-type': 'text/html' } }));
|
|
41
|
+
const issues = await new GoogleAdsProgramDriver(user, ctx).validateProposal(program({ creativeImageUrl: 'https://example.test/page' }));
|
|
42
|
+
const hit = issues.find(i => i.code === 'GA_CREATIVE_IMAGE_INVALID');
|
|
43
|
+
expect(hit?.severity).toBe('error');
|
|
44
|
+
expect(hit?.message).toMatch(/PNG or JPEG required/);
|
|
45
|
+
});
|
|
46
|
+
it('does not fetch anything when no image lever is set', async () => {
|
|
47
|
+
const f = vi.fn();
|
|
48
|
+
vi.stubGlobal('fetch', f);
|
|
49
|
+
await new GoogleAdsProgramDriver(user, ctx).validateProposal(program({}));
|
|
50
|
+
expect(f).not.toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
//# sourceMappingURL=googleCreativeImage.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"googleCreativeImage.test.js","sourceRoot":"","sources":["../../src/__tests__/googleCreativeImage.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linkedInTargeting.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/linkedInTargeting.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { creativeRetireTarget, inlineCreativeContent, lacksConversionScope } from '../programs/linkedin/LinkedInAdsClient.js';
|
|
3
|
+
import { withRequiredFacets } from '../programs/linkedin/LinkedInAdsClient.js';
|
|
4
|
+
/**
|
|
5
|
+
* R1 (plans/linkedin-ad-setup-plan.md): a campaign's targeting is the saved audience's own criteria — plus the
|
|
6
|
+
* location/locale pair LinkedIn requires when the template lacks it, plus the retargeting segment when the audience
|
|
7
|
+
* lever asked for one. CP-02's criteria were the pair ALONE (300M people); this is what makes that impossible.
|
|
8
|
+
*/
|
|
9
|
+
const staff = {
|
|
10
|
+
include: { and: [
|
|
11
|
+
{ or: { 'urn:li:adTargetingFacet:jobFunctions': ['urn:li:function:7'] } },
|
|
12
|
+
{ or: { 'urn:li:adTargetingFacet:locations': ['urn:li:geo:103644278', 'urn:li:geo:101174742'] } },
|
|
13
|
+
{ or: { 'urn:li:adTargetingFacet:interfaceLocales': ['urn:li:locale:en_US'] } },
|
|
14
|
+
] },
|
|
15
|
+
exclude: { or: { 'urn:li:adTargetingFacet:industries': ['urn:li:industry:68'] } },
|
|
16
|
+
};
|
|
17
|
+
const facetsOf = (c) => (c['include'].and).map(a => Object.keys(a.or)[0]);
|
|
18
|
+
describe('withRequiredFacets', () => {
|
|
19
|
+
it('copies the template verbatim when it already carries a location and a locale, exclude included', () => {
|
|
20
|
+
const out = withRequiredFacets(staff, null);
|
|
21
|
+
expect(facetsOf(out)).toEqual(['urn:li:adTargetingFacet:jobFunctions', 'urn:li:adTargetingFacet:locations', 'urn:li:adTargetingFacet:interfaceLocales']);
|
|
22
|
+
expect(out['exclude']).toEqual(staff.exclude);
|
|
23
|
+
});
|
|
24
|
+
it('adds the required location and locale when the template lacks them', () => {
|
|
25
|
+
const out = withRequiredFacets({ include: { and: [{ or: { 'urn:li:adTargetingFacet:industries': ['urn:li:industry:100'] } }] } }, null);
|
|
26
|
+
expect(facetsOf(out)).toEqual(['urn:li:adTargetingFacet:industries', 'urn:li:adTargetingFacet:locations', 'urn:li:adTargetingFacet:interfaceLocales']);
|
|
27
|
+
});
|
|
28
|
+
it('AND-s the retargeting segment in through the dynamic facet, first', () => {
|
|
29
|
+
const out = withRequiredFacets(staff, 'urn:li:adSegment:42');
|
|
30
|
+
expect(facetsOf(out)[0]).toBe('urn:li:adTargetingFacet:dynamicSegments');
|
|
31
|
+
expect(facetsOf(out)).toHaveLength(4);
|
|
32
|
+
});
|
|
33
|
+
it('never mutates the template object', () => {
|
|
34
|
+
const before = JSON.stringify(staff);
|
|
35
|
+
withRequiredFacets(staff, 'urn:li:adSegment:42');
|
|
36
|
+
expect(JSON.stringify(staff)).toBe(before);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('creativeRetireTarget (KI-146)', () => {
|
|
40
|
+
it('archives an approved creative and leaves one LinkedIn has not approved untouched (no transition is allowed before approval)', () => {
|
|
41
|
+
expect(creativeRetireTarget({ intendedStatus: 'ACTIVE', review: { status: 'APPROVED' } })).toEqual({ status: 'ARCHIVED', change: true });
|
|
42
|
+
expect(creativeRetireTarget({ intendedStatus: 'ACTIVE', review: { status: 'REJECTED' } })).toEqual({ status: 'HELD', change: false });
|
|
43
|
+
expect(creativeRetireTarget({ intendedStatus: 'ACTIVE', review: { status: 'PENDING' } })).toEqual({ status: 'HELD', change: false });
|
|
44
|
+
expect(creativeRetireTarget({ intendedStatus: 'ACTIVE' })).toEqual({ status: 'HELD', change: false });
|
|
45
|
+
});
|
|
46
|
+
it('changes nothing that is already archived', () => {
|
|
47
|
+
expect(creativeRetireTarget({ intendedStatus: 'ARCHIVED', review: { status: 'REJECTED' } })).toEqual({ status: 'ARCHIVED', change: false });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('KI-148: a conversion needs the rw_conversions scope', () => {
|
|
51
|
+
it('a known scope list without it is a miss; with it, or unknown, it is not', () => {
|
|
52
|
+
expect(lacksConversionScope(['r_ads', 'rw_ads', 'r_ads_reporting'])).toBe(true);
|
|
53
|
+
expect(lacksConversionScope(['r_ads', 'rw_ads', 'rw_conversions'])).toBe(false);
|
|
54
|
+
expect(lacksConversionScope(null)).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('KI-150: an ad is article content with the image as thumbnail — the shape Campaign Manager renders with headline, destination and button', () => {
|
|
58
|
+
it('with an image: source, title, description, thumbnail + alt text; without: a plain link card', () => {
|
|
59
|
+
expect(inlineCreativeContent({ landingPageUrl: 'https://x.test/a?utm_source=linkedin', headline: 'H', description: 'D', imageUrn: 'urn:li:image:1' }))
|
|
60
|
+
.toEqual({ article: { source: 'https://x.test/a?utm_source=linkedin', title: 'H', description: 'D', thumbnail: 'urn:li:image:1', thumbnailAltText: 'H' } });
|
|
61
|
+
expect(inlineCreativeContent({ landingPageUrl: 'https://x.test/a', headline: 'H', description: null, imageUrn: null }))
|
|
62
|
+
.toEqual({ article: { source: 'https://x.test/a', title: 'H' } });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
//# sourceMappingURL=linkedInTargeting.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linkedInTargeting.test.js","sourceRoot":"","sources":["../../src/__tests__/linkedInTargeting.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseMarketingAgent } from './BaseMarketingAgent.js';
|
|
2
|
+
/**
|
|
3
|
+
* Marketing Creative Agent — driver class (D-29).
|
|
4
|
+
*
|
|
5
|
+
* The Programs agent's SUB-AGENT for the picture in an ad: it decides what the image shows and which candidate
|
|
6
|
+
* fits the brand, through `Generate Ad Image` (generate, or adapt a supplied picture). It is a child of the Programs
|
|
7
|
+
* agent in metadata (`ParentID`), so MJ discovers it from the relationship and the parent delegates when a draft
|
|
8
|
+
* needs an image. The base class gives it what every marketing agent has: the brand from the payload or the
|
|
9
|
+
* caller's grants, and the payload bootstrap. Nothing else is special about it — the judgment is in its prompt.
|
|
10
|
+
*/
|
|
11
|
+
/** The agent row's Name — what the Programs agent's delegation names and its sub-agent hook matches on (KI-134). */
|
|
12
|
+
export declare const CREATIVE_AGENT_NAME = "Marketing Creative Agent";
|
|
13
|
+
export declare class MarketingCreativeAgent extends BaseMarketingAgent {
|
|
14
|
+
}
|
|
15
|
+
export declare function LoadMarketingCreativeAgent(): void;
|
|
16
|
+
//# sourceMappingURL=MarketingCreativeAgent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MarketingCreativeAgent.d.ts","sourceRoot":"","sources":["../../src/agents/MarketingCreativeAgent.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D;;;;;;;;GAQG;AACH,oHAAoH;AACpH,eAAO,MAAM,mBAAmB,6BAA6B,CAAC;AAE9D,qBACa,sBAAuB,SAAQ,kBAAkB;CAC7D;AAED,wBAAgB,0BAA0B,SAAK"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
8
|
+
import { BaseAgent } from '@memberjunction/ai-agents';
|
|
9
|
+
import { BaseMarketingAgent } from './BaseMarketingAgent.js';
|
|
10
|
+
/**
|
|
11
|
+
* Marketing Creative Agent — driver class (D-29).
|
|
12
|
+
*
|
|
13
|
+
* The Programs agent's SUB-AGENT for the picture in an ad: it decides what the image shows and which candidate
|
|
14
|
+
* fits the brand, through `Generate Ad Image` (generate, or adapt a supplied picture). It is a child of the Programs
|
|
15
|
+
* agent in metadata (`ParentID`), so MJ discovers it from the relationship and the parent delegates when a draft
|
|
16
|
+
* needs an image. The base class gives it what every marketing agent has: the brand from the payload or the
|
|
17
|
+
* caller's grants, and the payload bootstrap. Nothing else is special about it — the judgment is in its prompt.
|
|
18
|
+
*/
|
|
19
|
+
/** The agent row's Name — what the Programs agent's delegation names and its sub-agent hook matches on (KI-134). */
|
|
20
|
+
export const CREATIVE_AGENT_NAME = 'Marketing Creative Agent';
|
|
21
|
+
let MarketingCreativeAgent = class MarketingCreativeAgent extends BaseMarketingAgent {
|
|
22
|
+
};
|
|
23
|
+
MarketingCreativeAgent = __decorate([
|
|
24
|
+
RegisterClass(BaseAgent, 'MarketingCreativeAgent')
|
|
25
|
+
], MarketingCreativeAgent);
|
|
26
|
+
export { MarketingCreativeAgent };
|
|
27
|
+
export function LoadMarketingCreativeAgent() { }
|
|
28
|
+
//# sourceMappingURL=MarketingCreativeAgent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MarketingCreativeAgent.js","sourceRoot":"","sources":["../../src/agents/MarketingCreativeAgent.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D;;;;;;;;GAQG;AACH,oHAAoH;AACpH,MAAM,CAAC,MAAM,mBAAmB,GAAG,0BAA0B,CAAC;AAGvD,IAAM,sBAAsB,GAA5B,MAAM,sBAAuB,SAAQ,kBAAkB;CAC7D,CAAA;AADY,sBAAsB;IADlC,aAAa,CAAC,SAAS,EAAE,wBAAwB,CAAC;GACtC,sBAAsB,CAClC;;AAED,MAAM,UAAU,0BAA0B,KAAI,CAAC"}
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import type { AgentConfiguration, AIPromptParams, ExecuteAgentParams, BaseAgentNextStep, MJAIAgentRunEntityExtended, MJAIAgentRunStepEntityExtended } from '@memberjunction/ai-core-plus';
|
|
2
2
|
import { BaseMarketingAgent } from './BaseMarketingAgent.js';
|
|
3
|
+
/**
|
|
4
|
+
* One proposal a run produced, as stamped on the payload for the proposal card.
|
|
5
|
+
* `draft` is set when nothing was saved (the in-app Assistant); otherwise `programId` names the row.
|
|
6
|
+
*/
|
|
7
|
+
interface ProposalRef {
|
|
8
|
+
programId: string | null;
|
|
9
|
+
code: string | null;
|
|
10
|
+
name: string | null;
|
|
11
|
+
approvalRequestId: string | null;
|
|
12
|
+
draft: {
|
|
13
|
+
/** Identifies the draft so the user's decision can be recorded against it. */
|
|
14
|
+
key: string;
|
|
15
|
+
/** The exact `Propose Program` inputs that passed validation — re-submitted on Approve. */
|
|
16
|
+
inputs: Record<string, unknown>;
|
|
17
|
+
programTier: string | null;
|
|
18
|
+
programType: string | null;
|
|
19
|
+
proposalConfidence: string | null;
|
|
20
|
+
requiresOverride: boolean;
|
|
21
|
+
} | null;
|
|
22
|
+
}
|
|
3
23
|
/**
|
|
4
24
|
* Marketing Programs Agent — driver class.
|
|
5
25
|
*
|
|
@@ -90,6 +110,17 @@ export declare class MarketingExperimentationAgent extends BaseMarketingAgent {
|
|
|
90
110
|
* the gate; the entity runs it on save) — this is the cheaper, earlier door, not the only one.
|
|
91
111
|
*/
|
|
92
112
|
protected validateActionsNextStep<P>(params: ExecuteAgentParams, nextStep: BaseAgentNextStep<P>, currentPayload: P, agentRun: MJAIAgentRunEntityExtended, currentStep: MJAIAgentRunStepEntityExtended): Promise<BaseAgentNextStep<P>>;
|
|
113
|
+
/**
|
|
114
|
+
* A Creative delegation never ends the parent's run (KI-134) — MJ's `validateSubAgentNextStep` hook, the
|
|
115
|
+
* sub-agent twin of the action hook above. On the adapt follow-up ("use the Ascend cover, make it square") the
|
|
116
|
+
* model delegated with `terminateAfter: true`; the framework honoured it, so the run ended the moment the
|
|
117
|
+
* Creative agent returned: the square image was generated and hosted, `payload.creative` carried it, but the
|
|
118
|
+
* founder saw a bare "completed" with no message and `creativeImageUrl` never changed. The parent is the only
|
|
119
|
+
* one who can merge `creative` into the levers and answer, so the delegation is forced to hand control back.
|
|
120
|
+
*/
|
|
121
|
+
protected validateSubAgentNextStep<P>(params: ExecuteAgentParams, nextStep: BaseAgentNextStep<P>, currentPayload: P, agentRun: MJAIAgentRunEntityExtended, currentStep: MJAIAgentRunStepEntityExtended): Promise<BaseAgentNextStep<P>>;
|
|
122
|
+
/** The provenance verdict for a proposal's creativeImageUrl, or null when there is nothing to bounce. */
|
|
123
|
+
private creativeImageBounce;
|
|
93
124
|
/**
|
|
94
125
|
* Decides, in code, whether this run may SAVE a proposal or only DRAFT one.
|
|
95
126
|
*
|
|
@@ -134,4 +165,12 @@ export declare class MarketingExperimentationAgent extends BaseMarketingAgent {
|
|
|
134
165
|
}
|
|
135
166
|
/** Anti-tree-shaking loader — the @RegisterClass above is a side effect. */
|
|
136
167
|
export declare function LoadMarketingExperimentationAgent(): void;
|
|
168
|
+
/**
|
|
169
|
+
* One card per program (KI-124): the model sometimes calls Propose Program twice in a run for the same program (a
|
|
170
|
+
* revision after the user's follow-up, or a plain repeat — CP-Meta drafted twice three seconds apart), and the card
|
|
171
|
+
* showed "(+1 more)" with two identical drafts. The LATER call is the proposal: drafts collapse by name, committed
|
|
172
|
+
* programs by id. Steps arrive in StepNumber order, so last wins.
|
|
173
|
+
*/
|
|
174
|
+
export declare function dedupeProposals(proposals: ProposalRef[]): ProposalRef[];
|
|
175
|
+
export {};
|
|
137
176
|
//# sourceMappingURL=MarketingExperimentationAgent.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MarketingExperimentationAgent.d.ts","sourceRoot":"","sources":["../../src/agents/MarketingExperimentationAgent.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,0BAA0B,EAAE,8BAA8B,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"MarketingExperimentationAgent.d.ts","sourceRoot":"","sources":["../../src/agents/MarketingExperimentationAgent.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,8 +8,9 @@ var MarketingExperimentationAgent_1;
|
|
|
8
8
|
import { EscapeSQLString, RegisterClass } from '@memberjunction/global';
|
|
9
9
|
import { BaseAgent } from '@memberjunction/ai-agents';
|
|
10
10
|
import { LogStatus, LogError, Metadata, RunView } from '@memberjunction/core';
|
|
11
|
-
import { blockingLearningErrors, blockingErrors, driverProposalIssues, ProgramMetadataEngine, decidedProgramFromTaskInput, parseTaskInput, enrichFinding, loadProgramKnowledge, renderKnowledgeBase, toMarketingEntityName, validateLearning, } from '@mj-biz-apps/marketing-core';
|
|
11
|
+
import { blockingLearningErrors, blockingErrors, driverProposalIssues, ProgramMetadataEngine, decidedProgramFromTaskInput, parseTaskInput, enrichFinding, loadProgramKnowledge, renderKnowledgeBase, toMarketingEntityName, validateLearning, creativeImageProvenance, } from '@mj-biz-apps/marketing-core';
|
|
12
12
|
import { BaseMarketingAgent } from './BaseMarketingAgent.js';
|
|
13
|
+
import { CREATIVE_AGENT_NAME } from './MarketingCreativeAgent.js';
|
|
13
14
|
/** Below this, a request is too short to rank a corpus against. */
|
|
14
15
|
const MIN_RETRIEVAL_QUERY_CHARS = 25;
|
|
15
16
|
/**
|
|
@@ -255,6 +256,13 @@ let MarketingExperimentationAgent = class MarketingExperimentationAgent extends
|
|
|
255
256
|
continue; // Propose Program reports an unresolved type itself
|
|
256
257
|
const levers = p['TacticConfigJSON'];
|
|
257
258
|
const tacticConfigJSON = typeof levers === 'string' ? levers : levers != null ? JSON.stringify(levers) : null;
|
|
259
|
+
// KI-131: an ad image URL must be one this run RECEIVED — the Creative Agent's payload block or the
|
|
260
|
+
// founder's words. The first live run invented one; the prompt's "never invent" is advice, this is the rule.
|
|
261
|
+
const imageBounce = this.creativeImageBounce(tacticConfigJSON, bag, params);
|
|
262
|
+
if (imageBounce) {
|
|
263
|
+
LogStatus(`MarketingExperimentationAgent: Propose Program call bounced — creativeImageUrl provenance`);
|
|
264
|
+
return { step: 'Retry', terminate: false, errorMessage: imageBounce };
|
|
265
|
+
}
|
|
258
266
|
const issues = blockingErrors(await driverProposalIssues({
|
|
259
267
|
Name: str('Name'), Hypothesis: str('Hypothesis'), MetricName: str('MetricName'), MetricBaseline: num('MetricBaseline'),
|
|
260
268
|
Budget: num('Budget'), SegmentCuts: str('SegmentCuts'), ProgramTypeID: typeRow.ID, TacticConfigJSON: tacticConfigJSON,
|
|
@@ -276,6 +284,45 @@ let MarketingExperimentationAgent = class MarketingExperimentationAgent extends
|
|
|
276
284
|
}
|
|
277
285
|
return base;
|
|
278
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* A Creative delegation never ends the parent's run (KI-134) — MJ's `validateSubAgentNextStep` hook, the
|
|
289
|
+
* sub-agent twin of the action hook above. On the adapt follow-up ("use the Ascend cover, make it square") the
|
|
290
|
+
* model delegated with `terminateAfter: true`; the framework honoured it, so the run ended the moment the
|
|
291
|
+
* Creative agent returned: the square image was generated and hosted, `payload.creative` carried it, but the
|
|
292
|
+
* founder saw a bare "completed" with no message and `creativeImageUrl` never changed. The parent is the only
|
|
293
|
+
* one who can merge `creative` into the levers and answer, so the delegation is forced to hand control back.
|
|
294
|
+
*/
|
|
295
|
+
async validateSubAgentNextStep(params, nextStep, currentPayload, agentRun, currentStep) {
|
|
296
|
+
const base = await super.validateSubAgentNextStep(params, nextStep, currentPayload, agentRun, currentStep);
|
|
297
|
+
if (base.step !== nextStep.step)
|
|
298
|
+
return base; // the framework already refused it
|
|
299
|
+
for (const req of this.getRequestedSubAgents(base)) {
|
|
300
|
+
if (req.name.trim().toLowerCase() === CREATIVE_AGENT_NAME.toLowerCase() && req.terminateAfter) {
|
|
301
|
+
LogStatus(`MarketingExperimentationAgent: ${CREATIVE_AGENT_NAME} delegation asked to terminate the run — overridden, the parent must merge the creative and answer (KI-134)`);
|
|
302
|
+
req.terminateAfter = false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return base;
|
|
306
|
+
}
|
|
307
|
+
/** The provenance verdict for a proposal's creativeImageUrl, or null when there is nothing to bounce. */
|
|
308
|
+
creativeImageBounce(tacticConfigJSON, bag, params) {
|
|
309
|
+
if (!tacticConfigJSON)
|
|
310
|
+
return null;
|
|
311
|
+
let url;
|
|
312
|
+
try {
|
|
313
|
+
url = JSON.parse(tacticConfigJSON)['creativeImageUrl'];
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return null;
|
|
317
|
+
} // the parser reports bad JSON
|
|
318
|
+
if (typeof url !== 'string' || !url.trim())
|
|
319
|
+
return null;
|
|
320
|
+
const founderTexts = (params.conversationMessages ?? [])
|
|
321
|
+
.filter(m => m.role === 'user')
|
|
322
|
+
.map(m => typeof m.content === 'string' ? m.content : m.content.map(b => (b.type === 'text' ? b.content : '')).join('\n'));
|
|
323
|
+
const verdict = creativeImageProvenance(url.trim(), { payloadCreative: bag['creative'], founderTexts });
|
|
324
|
+
return verdict.ok ? null : verdict.message;
|
|
325
|
+
}
|
|
279
326
|
/**
|
|
280
327
|
* Decides, in code, whether this run may SAVE a proposal or only DRAFT one.
|
|
281
328
|
*
|
|
@@ -319,9 +366,9 @@ let MarketingExperimentationAgent = class MarketingExperimentationAgent extends
|
|
|
319
366
|
LogError(`MarketingExperimentationAgent: could not read this run's proposal steps — ${res.ErrorMessage}`);
|
|
320
367
|
return;
|
|
321
368
|
}
|
|
322
|
-
const proposals = (res.Results ?? [])
|
|
369
|
+
const proposals = dedupeProposals((res.Results ?? [])
|
|
323
370
|
.map(step => this.proposalFromStep(step.OutputData))
|
|
324
|
-
.filter((p) => p !== null);
|
|
371
|
+
.filter((p) => p !== null));
|
|
325
372
|
if (proposals.length === 0)
|
|
326
373
|
return;
|
|
327
374
|
bag['proposals'] = proposals;
|
|
@@ -358,6 +405,14 @@ let MarketingExperimentationAgent = class MarketingExperimentationAgent extends
|
|
|
358
405
|
if (!draftKey)
|
|
359
406
|
return null;
|
|
360
407
|
delete inputs['DraftOnly']; // the re-submit on Approve must COMMIT
|
|
408
|
+
// D-32: budgets are computed by Propose; the card shows the computed figures, not what the model typed.
|
|
409
|
+
if (outputs['ComputedBudget'] !== undefined)
|
|
410
|
+
inputs['Budget'] = outputs['ComputedBudget'];
|
|
411
|
+
if (outputs['ComputedBudgetPerSprint'] !== undefined)
|
|
412
|
+
inputs['BudgetPerSprint'] = outputs['ComputedBudgetPerSprint'];
|
|
413
|
+
// D-34: the levers with the platform's names cached beside the ids — what the card and the commit carry.
|
|
414
|
+
if (typeof outputs['ResolvedTacticConfigJSON'] === 'string' && outputs['ResolvedTacticConfigJSON'])
|
|
415
|
+
inputs['TacticConfigJSON'] = outputs['ResolvedTacticConfigJSON'];
|
|
361
416
|
return {
|
|
362
417
|
programId: null, code: null, name: text(inputs['Name']), approvalRequestId: null,
|
|
363
418
|
draft: {
|
|
@@ -524,4 +579,19 @@ MarketingExperimentationAgent = MarketingExperimentationAgent_1 = __decorate([
|
|
|
524
579
|
export { MarketingExperimentationAgent };
|
|
525
580
|
/** Anti-tree-shaking loader — the @RegisterClass above is a side effect. */
|
|
526
581
|
export function LoadMarketingExperimentationAgent() { }
|
|
582
|
+
/**
|
|
583
|
+
* One card per program (KI-124): the model sometimes calls Propose Program twice in a run for the same program (a
|
|
584
|
+
* revision after the user's follow-up, or a plain repeat — CP-Meta drafted twice three seconds apart), and the card
|
|
585
|
+
* showed "(+1 more)" with two identical drafts. The LATER call is the proposal: drafts collapse by name, committed
|
|
586
|
+
* programs by id. Steps arrive in StepNumber order, so last wins.
|
|
587
|
+
*/
|
|
588
|
+
export function dedupeProposals(proposals) {
|
|
589
|
+
const byKey = new Map();
|
|
590
|
+
for (const p of proposals) {
|
|
591
|
+
const key = p.programId ? `id:${p.programId.toLowerCase()}` : `name:${(p.name ?? '').trim().toLowerCase()}`;
|
|
592
|
+
byKey.delete(key);
|
|
593
|
+
byKey.set(key, p);
|
|
594
|
+
}
|
|
595
|
+
return [...byKey.values()];
|
|
596
|
+
}
|
|
527
597
|
//# sourceMappingURL=MarketingExperimentationAgent.js.map
|