ebay-mcp-remote-edition 3.2.0 → 3.3.1

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.
@@ -0,0 +1,30 @@
1
+ import { buildResolvedValidationQueryPlan } from './query-utils.js';
2
+ export async function getTerapeakValidationSignals(_api, request) {
3
+ await Promise.resolve();
4
+ const { queryPlan, queryResolution } = buildResolvedValidationQueryPlan(request);
5
+ const queryCandidates = queryPlan.map((candidate) => candidate.query);
6
+ const currentQuery = queryCandidates[0] ?? null;
7
+ const previousPobQuery = queryCandidates[1] ?? null;
8
+ return {
9
+ avgWatchersPerListing: null,
10
+ preOrderListingsCount: null,
11
+ marketPriceUsd: null,
12
+ avgShippingCostUsd: null,
13
+ competitionLevel: null,
14
+ previousPobAvgPriceUsd: null,
15
+ previousPobSellThroughPct: null,
16
+ currentListingsCount: null,
17
+ soldListingsCount: null,
18
+ provider: 'none',
19
+ confidence: 'Low',
20
+ queryDebug: {
21
+ currentQuery,
22
+ previousPobQuery,
23
+ selectedMode: 'combined',
24
+ currentResultCount: null,
25
+ previousPobResultCount: null,
26
+ queryResolution,
27
+ notes: 'Terapeak/eBay research provider contract is in place, but live authenticated research retrieval is not implemented yet.',
28
+ },
29
+ };
30
+ }
@@ -13,11 +13,22 @@ export function buildValidationRecommendation(request, signals) {
13
13
  : trackingCadence === 'Hourly'
14
14
  ? addHours(request.timestamp, 1)
15
15
  : addHours(request.timestamp, 24);
16
- const marketPrice = signals.sold.soldMedianPriceUsd ?? signals.ebay.marketPriceUsd;
17
- const wholesale = request.item.wholesalePrice;
16
+ const marketPrice = signals.terapeak.marketPriceUsd ??
17
+ signals.sold.soldMedianPriceUsd ??
18
+ signals.ebay.marketPriceUsd;
19
+ const preorderListingsCount = signals.terapeak.preOrderListingsCount ?? signals.ebay.preOrderListingsCount;
20
+ const wholesale = signals.effectiveContext.hasItem ? request.item.wholesalePrice : null;
18
21
  const marginRatio = marketPrice !== null && wholesale !== null && wholesale > 0
19
22
  ? (marketPrice - wholesale) / wholesale
20
23
  : null;
24
+ const subjectLabel = signals.effectiveContext.sourceType === 'event'
25
+ ? (signals.effectiveContext.searchEvent ??
26
+ signals.effectiveContext.effectiveSearchQuery ??
27
+ 'event opportunity')
28
+ : (signals.effectiveContext.searchAlbum ??
29
+ signals.effectiveContext.searchItem ??
30
+ request.item.name ??
31
+ 'release');
21
32
  const recentSoldCount = [
22
33
  signals.sold.soldVelocity.day1Sold,
23
34
  signals.sold.soldVelocity.day2Sold,
@@ -25,18 +36,17 @@ export function buildValidationRecommendation(request, signals) {
25
36
  ].reduce((sum, value) => sum + (value ?? 0), 0);
26
37
  let latestAiRecommendation = 'Continue watching until stronger market signal appears.';
27
38
  let latestAiConfidence = 'Medium';
28
- let monitoringNotes = 'Baseline recommendation generated from current validation state.';
39
+ let monitoringNotes = `Baseline recommendation generated from current ${signals.effectiveContext.mode} validation state for ${subjectLabel}.`;
29
40
  if (!shouldAutoTrack) {
30
41
  latestAiRecommendation =
31
42
  'Automatic tracking paused because the validation is no longer in a watchable state.';
32
43
  latestAiConfidence = 'High';
33
- monitoringNotes =
34
- 'Stop conditions were met, so automation will not schedule another validation run.';
44
+ monitoringNotes = `Stop conditions were met, so automation will not schedule another ${signals.effectiveContext.mode} validation run for ${subjectLabel}.`;
35
45
  }
36
46
  else if (marginRatio !== null &&
37
47
  marginRatio >= 1 &&
38
- signals.ebay.preOrderListingsCount !== null &&
39
- signals.ebay.preOrderListingsCount >= 25) {
48
+ preorderListingsCount !== null &&
49
+ preorderListingsCount >= 25) {
40
50
  latestAiRecommendation =
41
51
  'Demand and pricing look constructive. Continue tracking closely and be ready to upgrade from watch status if sell-through strengthens.';
42
52
  latestAiConfidence = 'High';
@@ -63,13 +73,26 @@ export function buildValidationRecommendation(request, signals) {
63
73
  monitoringNotes =
64
74
  'Temporary sold-provider data is present, but sample depth is not yet strong enough to justify an automatic buy-decision change.';
65
75
  }
66
- else if (signals.social.youtubeViews24hMillions !== null ||
67
- signals.social.redditPostsCount7d !== null) {
76
+ else if (signals.social.twitterTrending === true ||
77
+ (signals.social.youtubeViews24hMillions !== null &&
78
+ signals.social.youtubeViews24hMillions >= 0.1) ||
79
+ (signals.social.redditPostsCount7d !== null && signals.social.redditPostsCount7d >= 5)) {
68
80
  latestAiRecommendation =
69
81
  'Demand signals are mixed. Keep monitoring until eBay pricing and social momentum become more decisive.';
70
82
  latestAiConfidence = 'Medium';
71
83
  monitoringNotes =
72
- 'Cross-channel activity exists, but the combined signal is not strong enough for an automatic buy change.';
84
+ 'Cross-channel social activity exists, but it is only being used as a supporting confidence signal and is not strong enough to justify an automatic buy change.';
85
+ }
86
+ if (signals.terapeak.previousPobSellThroughPct !== null &&
87
+ signals.terapeak.previousPobSellThroughPct >= 50) {
88
+ monitoringNotes +=
89
+ ' Previous POB sell-through research suggests the comparable release sold through efficiently.';
90
+ if (latestAiConfidence !== 'High') {
91
+ latestAiConfidence = 'Medium';
92
+ }
93
+ }
94
+ if (signals.research.previousComebackFirstWeekSales !== null) {
95
+ monitoringNotes += ` Previous comeback first-week sales reference: ${signals.research.previousComebackFirstWeekSales}.`;
73
96
  }
74
97
  return {
75
98
  buyDecision: request.validation.buyDecision,
@@ -1,9 +1,13 @@
1
1
  import { validationRunRequestSchema } from './schemas.js';
2
2
  import { getEbayValidationSignals } from './providers/ebay.js';
3
3
  import { getEbaySoldValidationSignals } from './providers/ebay-sold.js';
4
+ import { getTerapeakValidationSignals } from './providers/terapeak.js';
4
5
  import { getSocialValidationSignals } from './providers/social.js';
5
6
  import { getChartValidationSignals } from './providers/chart.js';
7
+ import { getPreviousComebackResearchSignals } from './providers/research.js';
8
+ import { buildProviderQueryResolutionDebug } from './providers/query-utils.js';
6
9
  import { buildValidationRecommendation } from './recommendation.js';
10
+ import { buildValidationEffectiveContext } from './effective-context.js';
7
11
  function addMinutes(timestamp, minutes) {
8
12
  return new Date(new Date(timestamp).getTime() + minutes * 60 * 1000).toISOString();
9
13
  }
@@ -23,35 +27,171 @@ function getValidationId(input) {
23
27
  }
24
28
  return '';
25
29
  }
26
- function buildProviderDebug(ebay, sold, social, chart) {
30
+ function isMeaningfulWriteValue(value) {
31
+ if (value === null || value === undefined) {
32
+ return false;
33
+ }
34
+ return typeof value !== 'string' || value.length > 0;
35
+ }
36
+ function getFieldPresence(fields) {
37
+ const contributed = [];
38
+ const omitted = [];
39
+ for (const [field, value] of Object.entries(fields)) {
40
+ if (isMeaningfulWriteValue(value)) {
41
+ contributed.push(field);
42
+ }
43
+ else {
44
+ omitted.push(field);
45
+ }
46
+ }
47
+ return { contributed, omitted };
48
+ }
49
+ function getWriteSource(value, source) {
50
+ return isMeaningfulWriteValue(value) ? source : 'none';
51
+ }
52
+ function buildProviderDebug(request, ebay, sold, terapeak, social, chart, research) {
53
+ const requestQueryResolution = buildProviderQueryResolutionDebug(request, Boolean(ebay.queryResolution?.queryContextUsed));
54
+ const ebayFields = getFieldPresence({
55
+ avgWatchersPerListing: ebay.avgWatchersPerListing,
56
+ preOrderListingsCount: ebay.preOrderListingsCount,
57
+ marketPriceUsd: ebay.marketPriceUsd,
58
+ avgShippingCostUsd: ebay.avgShippingCostUsd,
59
+ competitionLevel: ebay.competitionLevel,
60
+ });
61
+ const soldFields = getFieldPresence({
62
+ soldAveragePriceUsd: sold.soldAveragePriceUsd,
63
+ soldMedianPriceUsd: sold.soldMedianPriceUsd,
64
+ soldMinPriceUsd: sold.soldMinPriceUsd,
65
+ soldMaxPriceUsd: sold.soldMaxPriceUsd,
66
+ day1Sold: sold.soldVelocity.day1Sold,
67
+ day2Sold: sold.soldVelocity.day2Sold,
68
+ day3Sold: sold.soldVelocity.day3Sold,
69
+ day4Sold: sold.soldVelocity.day4Sold,
70
+ day5Sold: sold.soldVelocity.day5Sold,
71
+ daysTracked: sold.soldVelocity.daysTracked,
72
+ });
73
+ const terapeakFields = getFieldPresence({
74
+ avgWatchersPerListing: terapeak.avgWatchersPerListing,
75
+ preOrderListingsCount: terapeak.preOrderListingsCount,
76
+ marketPriceUsd: terapeak.marketPriceUsd,
77
+ avgShippingCostUsd: terapeak.avgShippingCostUsd,
78
+ competitionLevel: terapeak.competitionLevel,
79
+ previousPobAvgPriceUsd: terapeak.previousPobAvgPriceUsd,
80
+ previousPobSellThroughPct: terapeak.previousPobSellThroughPct,
81
+ });
82
+ const socialFields = getFieldPresence({
83
+ twitterTrending: social.twitterTrending,
84
+ youtubeViews24hMillions: social.youtubeViews24hMillions,
85
+ redditPostsCount7d: social.redditPostsCount7d,
86
+ });
87
+ const researchFields = getFieldPresence({
88
+ previousAlbumTitle: research.previousAlbumTitle,
89
+ previousComebackFirstWeekSales: research.previousComebackFirstWeekSales,
90
+ });
91
+ const ebayStatus = (ebay.queryCandidates?.length ?? 0) === 0
92
+ ? 'unavailable'
93
+ : ebay.sampleSize > 0
94
+ ? 'ok'
95
+ : 'partial';
96
+ const socialStatus = socialFields.contributed.length > 0 ? 'ok' : social.debug ? 'partial' : 'unavailable';
97
+ const terapeakStatus = terapeak.provider === 'none'
98
+ ? 'stub'
99
+ : terapeakFields.contributed.length > 0
100
+ ? 'ok'
101
+ : 'partial';
102
+ const researchStatus = research.previousComebackFirstWeekSales !== null || research.previousAlbumTitle !== null
103
+ ? 'ok'
104
+ : 'stub';
27
105
  return {
28
106
  ebay: {
29
- status: ebay.sampleSize > 0 ? 'ok' : 'partial',
107
+ status: ebayStatus,
30
108
  confidence: ebay.sampleSize >= 10 ? 'medium' : 'low',
31
- sampleSize: ebay.sampleSize,
32
- hasMarketPrice: ebay.marketPriceUsd !== null,
33
- hasShipping: ebay.avgShippingCostUsd !== null,
34
- hasWatchers: ebay.avgWatchersPerListing !== null,
109
+ browseSampleSize: ebay.sampleSize,
110
+ queryCandidates: ebay.queryCandidates ?? [],
111
+ selectedQuery: ebay.selectedQuery,
112
+ selectedQueryTier: ebay.selectedQueryTier,
113
+ queryDiagnostics: ebay.queryDiagnostics ?? [],
114
+ queryContextUsed: ebay.queryResolution?.queryContextUsed ?? requestQueryResolution.queryContextUsed,
115
+ querySource: ebay.queryResolution?.querySource ?? requestQueryResolution.querySource,
116
+ resolvedSearchQuery: ebay.queryResolution?.resolvedSearchQuery ?? requestQueryResolution.resolvedSearchQuery,
117
+ validationScope: ebay.queryResolution?.validationScope ?? requestQueryResolution.validationScope,
118
+ queryScope: ebay.queryResolution?.queryScope ?? requestQueryResolution.queryScope,
119
+ selectionReason: ebay.selectionReason,
120
+ errorMessage: ebay.errorMessage,
121
+ responseStatus: ebay.responseStatus,
122
+ responseBodyExcerpt: ebay.responseBodyExcerpt,
123
+ contributedFields: ebayFields.contributed,
124
+ omittedFields: ebayFields.omitted,
35
125
  },
36
126
  sold: {
37
127
  status: sold.status,
38
128
  provider: sold.provider,
39
129
  confidence: sold.confidence.toLowerCase(),
40
- results: sold.soldResultsCount,
41
- hasMedianPrice: sold.soldMedianPriceUsd !== null,
42
- hasVelocity: sold.soldVelocity.daysTracked !== null,
130
+ soldResultsCount: sold.soldResultsCount,
131
+ queryCandidates: sold.queryCandidates ?? [],
132
+ selectedQuery: sold.selectedQuery,
133
+ selectedQueryTier: sold.selectedQueryTier,
134
+ queryContextUsed: sold.queryResolution?.queryContextUsed ?? requestQueryResolution.queryContextUsed,
135
+ querySource: sold.queryResolution?.querySource ?? requestQueryResolution.querySource,
136
+ resolvedSearchQuery: sold.queryResolution?.resolvedSearchQuery ?? requestQueryResolution.resolvedSearchQuery,
137
+ validationScope: sold.queryResolution?.validationScope ?? requestQueryResolution.validationScope,
138
+ queryScope: sold.queryResolution?.queryScope ?? requestQueryResolution.queryScope,
139
+ contributedFields: soldFields.contributed,
140
+ omittedFields: soldFields.omitted,
141
+ errorMessage: sold.errorMessage,
142
+ },
143
+ terapeak: {
144
+ status: terapeakStatus,
145
+ provider: terapeak.provider,
146
+ confidence: terapeak.confidence.toLowerCase(),
147
+ queryCandidates: [
148
+ terapeak.queryDebug.currentQuery,
149
+ terapeak.queryDebug.previousPobQuery,
150
+ ].filter((value) => typeof value === 'string' && value.length > 0),
151
+ currentQuery: terapeak.queryDebug.currentQuery,
152
+ previousPobQuery: terapeak.queryDebug.previousPobQuery,
153
+ queryContextUsed: terapeak.queryDebug.queryResolution?.queryContextUsed ??
154
+ requestQueryResolution.queryContextUsed,
155
+ querySource: terapeak.queryDebug.queryResolution?.querySource ?? requestQueryResolution.querySource,
156
+ resolvedSearchQuery: terapeak.queryDebug.queryResolution?.resolvedSearchQuery ??
157
+ requestQueryResolution.resolvedSearchQuery,
158
+ validationScope: terapeak.queryDebug.queryResolution?.validationScope ??
159
+ requestQueryResolution.validationScope,
160
+ queryScope: terapeak.queryDebug.queryResolution?.queryScope ?? requestQueryResolution.queryScope,
161
+ selectedMode: terapeak.queryDebug.selectedMode,
162
+ currentResultCount: terapeak.queryDebug.currentResultCount,
163
+ previousPobResultCount: terapeak.queryDebug.previousPobResultCount,
164
+ contributedFields: terapeakFields.contributed,
165
+ omittedFields: terapeakFields.omitted,
166
+ notes: terapeak.queryDebug.notes,
43
167
  },
44
168
  social: {
45
- status: 'stub',
169
+ status: socialStatus,
46
170
  confidence: 'low',
47
- hasSignals: social.twitterTrending ||
48
- social.youtubeViews24hMillions !== null ||
49
- social.redditPostsCount7d !== null,
171
+ queryContextUsed: requestQueryResolution.queryContextUsed,
172
+ querySource: requestQueryResolution.querySource,
173
+ resolvedSearchQuery: requestQueryResolution.resolvedSearchQuery,
174
+ validationScope: requestQueryResolution.validationScope,
175
+ queryScope: requestQueryResolution.queryScope,
176
+ contributedFields: socialFields.contributed,
177
+ omittedFields: socialFields.omitted,
178
+ details: social.debug,
50
179
  },
51
180
  chart: {
52
181
  status: 'stub',
53
182
  confidence: 'low',
54
- hasSignals: Object.keys(chart).length > 0,
183
+ contributedFields: [],
184
+ omittedFields: ['chartMomentum'],
185
+ },
186
+ research: {
187
+ status: researchStatus,
188
+ confidence: research.confidence.toLowerCase(),
189
+ previousAlbumTitle: research.previousAlbumTitle,
190
+ previousComebackFirstWeekSales: research.previousComebackFirstWeekSales,
191
+ contributedFields: researchFields.contributed,
192
+ omittedFields: researchFields.omitted,
193
+ notes: research.notes,
194
+ sources: research.sources ?? [],
55
195
  },
56
196
  };
57
197
  }
@@ -71,11 +211,22 @@ export async function runValidation(api, input) {
71
211
  };
72
212
  }
73
213
  try {
74
- const ebay = await getEbayValidationSignals(api, request);
75
- const sold = await getEbaySoldValidationSignals(request);
76
- const social = getSocialValidationSignals(request);
77
- const chart = getChartValidationSignals(request);
78
- const marketPriceUsd = sold.soldMedianPriceUsd ?? ebay.marketPriceUsd;
214
+ const effectiveContext = buildValidationEffectiveContext(request);
215
+ const effectiveRequest = {
216
+ ...request,
217
+ effectiveContext,
218
+ };
219
+ const ebay = await getEbayValidationSignals(api, effectiveRequest);
220
+ const sold = await getEbaySoldValidationSignals(effectiveRequest);
221
+ const terapeak = await getTerapeakValidationSignals(api, effectiveRequest);
222
+ const social = await getSocialValidationSignals(effectiveRequest);
223
+ const chart = getChartValidationSignals(effectiveRequest);
224
+ const research = await getPreviousComebackResearchSignals(effectiveRequest);
225
+ const mergedAvgWatchers = terapeak.avgWatchersPerListing ?? ebay.avgWatchersPerListing;
226
+ const mergedPreorderListings = terapeak.preOrderListingsCount ?? ebay.preOrderListingsCount;
227
+ const marketPriceUsd = terapeak.marketPriceUsd ?? sold.soldMedianPriceUsd ?? ebay.marketPriceUsd;
228
+ const mergedAvgShippingCostUsd = terapeak.avgShippingCostUsd ?? ebay.avgShippingCostUsd;
229
+ const mergedCompetitionLevel = terapeak.competitionLevel ?? ebay.competitionLevel;
79
230
  const soldVelocity = {
80
231
  day1Sold: sold.soldVelocity.day1Sold ?? ebay.soldVelocity.day1Sold,
81
232
  day2Sold: sold.soldVelocity.day2Sold ?? ebay.soldVelocity.day2Sold,
@@ -84,20 +235,95 @@ export async function runValidation(api, input) {
84
235
  day5Sold: sold.soldVelocity.day5Sold ?? ebay.soldVelocity.day5Sold,
85
236
  daysTracked: sold.soldVelocity.daysTracked ?? ebay.soldVelocity.daysTracked,
86
237
  };
87
- const recommendation = buildValidationRecommendation(request, { ebay, sold, social, chart });
88
- const mergedSignals = { ebay, sold, social, chart };
238
+ const recommendation = buildValidationRecommendation(effectiveRequest, {
239
+ ebay,
240
+ sold,
241
+ terapeak,
242
+ social,
243
+ chart,
244
+ research,
245
+ effectiveContext,
246
+ });
247
+ const requestQueryResolution = buildProviderQueryResolutionDebug(effectiveRequest, Boolean(ebay.queryResolution?.queryContextUsed));
248
+ const mergedSignals = { effectiveContext, ebay, sold, terapeak, social, chart, research };
249
+ const socialWrites = {
250
+ ...(social.twitterTrending !== null ? { twitterTrending: social.twitterTrending } : {}),
251
+ ...(social.youtubeViews24hMillions !== null
252
+ ? { youtubeViews24hMillions: social.youtubeViews24hMillions }
253
+ : {}),
254
+ ...(social.redditPostsCount7d !== null
255
+ ? { redditPostsCount7d: social.redditPostsCount7d }
256
+ : {}),
257
+ };
258
+ const terapeakWrites = {
259
+ ...(terapeak.previousPobAvgPriceUsd !== null
260
+ ? { previousPobAvgPriceUsd: terapeak.previousPobAvgPriceUsd }
261
+ : {}),
262
+ ...(terapeak.previousPobSellThroughPct !== null
263
+ ? { previousPobSellThroughPct: terapeak.previousPobSellThroughPct }
264
+ : {}),
265
+ };
266
+ const researchWrites = {
267
+ ...(research.previousComebackFirstWeekSales !== null
268
+ ? { previousComebackFirstWeekSales: research.previousComebackFirstWeekSales }
269
+ : {}),
270
+ };
271
+ const writeResolution = {
272
+ avgWatchersPerListing: terapeak.avgWatchersPerListing !== null
273
+ ? 'terapeak'
274
+ : getWriteSource(ebay.avgWatchersPerListing, 'ebay'),
275
+ preOrderListingsCount: terapeak.preOrderListingsCount !== null
276
+ ? 'terapeak'
277
+ : getWriteSource(ebay.preOrderListingsCount, 'ebay'),
278
+ marketPriceUsd: terapeak.marketPriceUsd !== null
279
+ ? 'terapeak'
280
+ : sold.soldMedianPriceUsd !== null
281
+ ? 'sold'
282
+ : getWriteSource(ebay.marketPriceUsd, 'ebay'),
283
+ avgShippingCostUsd: terapeak.avgShippingCostUsd !== null
284
+ ? 'terapeak'
285
+ : getWriteSource(ebay.avgShippingCostUsd, 'ebay'),
286
+ competitionLevel: terapeak.competitionLevel !== null
287
+ ? 'terapeak'
288
+ : getWriteSource(ebay.competitionLevel, 'ebay'),
289
+ twitterTrending: getWriteSource(social.twitterTrending, 'social'),
290
+ youtubeViews24hMillions: getWriteSource(social.youtubeViews24hMillions, 'social'),
291
+ redditPostsCount7d: getWriteSource(social.redditPostsCount7d, 'social'),
292
+ day1Sold: sold.soldVelocity.day1Sold !== null
293
+ ? 'sold'
294
+ : getWriteSource(ebay.soldVelocity.day1Sold, 'ebay'),
295
+ day2Sold: sold.soldVelocity.day2Sold !== null
296
+ ? 'sold'
297
+ : getWriteSource(ebay.soldVelocity.day2Sold, 'ebay'),
298
+ day3Sold: sold.soldVelocity.day3Sold !== null
299
+ ? 'sold'
300
+ : getWriteSource(ebay.soldVelocity.day3Sold, 'ebay'),
301
+ day4Sold: sold.soldVelocity.day4Sold !== null
302
+ ? 'sold'
303
+ : getWriteSource(ebay.soldVelocity.day4Sold, 'ebay'),
304
+ day5Sold: sold.soldVelocity.day5Sold !== null
305
+ ? 'sold'
306
+ : getWriteSource(ebay.soldVelocity.day5Sold, 'ebay'),
307
+ daysTracked: sold.soldVelocity.daysTracked !== null
308
+ ? 'sold'
309
+ : getWriteSource(ebay.soldVelocity.daysTracked, 'ebay'),
310
+ previousPobAvgPriceUsd: getWriteSource(terapeak.previousPobAvgPriceUsd, 'terapeak'),
311
+ previousPobSellThroughPct: getWriteSource(terapeak.previousPobSellThroughPct, 'terapeak'),
312
+ previousComebackFirstWeekSales: getWriteSource(research.previousComebackFirstWeekSales, 'research'),
313
+ };
314
+ const omittedOptionalWrites = Object.entries(writeResolution)
315
+ .filter(([, source]) => source === 'none')
316
+ .map(([field]) => field);
89
317
  return {
90
318
  status: 'ok',
91
319
  validationId: request.validationId,
92
320
  writes: {
93
- avgWatchersPerListing: ebay.avgWatchersPerListing,
94
- preOrderListingsCount: ebay.preOrderListingsCount,
95
- twitterTrending: social.twitterTrending,
96
- youtubeViews24hMillions: social.youtubeViews24hMillions,
97
- redditPostsCount7d: social.redditPostsCount7d,
321
+ avgWatchersPerListing: mergedAvgWatchers,
322
+ preOrderListingsCount: mergedPreorderListings,
323
+ ...socialWrites,
98
324
  marketPriceUsd,
99
- avgShippingCostUsd: ebay.avgShippingCostUsd,
100
- competitionLevel: ebay.competitionLevel,
325
+ avgShippingCostUsd: mergedAvgShippingCostUsd,
326
+ competitionLevel: mergedCompetitionLevel,
101
327
  marketPriceTrend: ebay.marketPriceTrend,
102
328
  day1Sold: soldVelocity.day1Sold,
103
329
  day2Sold: soldVelocity.day2Sold,
@@ -105,6 +331,8 @@ export async function runValidation(api, input) {
105
331
  day4Sold: soldVelocity.day4Sold,
106
332
  day5Sold: soldVelocity.day5Sold,
107
333
  daysTracked: soldVelocity.daysTracked,
334
+ ...terapeakWrites,
335
+ ...researchWrites,
108
336
  monitoringNotes: recommendation.monitoringNotes,
109
337
  lastDataSnapshot: JSON.stringify(mergedSignals),
110
338
  latestAiRecommendation: recommendation.latestAiRecommendation,
@@ -119,11 +347,31 @@ export async function runValidation(api, input) {
119
347
  nextCheckAt: recommendation.nextCheckAt,
120
348
  },
121
349
  debug: {
350
+ sourceContext: effectiveRequest.sourceContext ?? null,
351
+ effectiveSourceType: effectiveContext.sourceType,
352
+ effectiveContextMode: effectiveContext.mode,
353
+ effectiveSearchQuery: effectiveContext.effectiveSearchQuery,
354
+ hasItem: effectiveContext.hasItem,
355
+ hasEvent: effectiveContext.hasEvent,
356
+ effectiveContext,
122
357
  ebayQuery: ebay.ebayQuery,
123
358
  soldQuery: sold.query,
124
- sampleSize: ebay.sampleSize,
125
- sourceSet: ['ebay', 'sold', 'social', 'chart'],
126
- providers: buildProviderDebug(ebay, sold, social, chart),
359
+ queryCandidates: {
360
+ ebay: ebay.queryCandidates ?? [],
361
+ sold: sold.queryCandidates ?? [],
362
+ terapeak: [terapeak.queryDebug.currentQuery, terapeak.queryDebug.previousPobQuery].filter((value) => typeof value === 'string' && value.length > 0),
363
+ },
364
+ browseSampleSize: ebay.sampleSize,
365
+ soldResultsCount: sold.soldResultsCount,
366
+ omittedOptionalWrites,
367
+ writeResolution,
368
+ sourceSet: ['ebay', 'sold', 'terapeak', 'social', 'chart', 'research'],
369
+ providers: buildProviderDebug(effectiveRequest, ebay, sold, terapeak, social, chart, research),
370
+ queryContextUsed: requestQueryResolution.queryContextUsed,
371
+ querySource: requestQueryResolution.querySource,
372
+ resolvedSearchQuery: requestQueryResolution.resolvedSearchQuery,
373
+ validationScope: requestQueryResolution.validationScope,
374
+ queryScope: requestQueryResolution.queryScope,
127
375
  },
128
376
  };
129
377
  }
@@ -17,25 +17,46 @@ export const validationCurrentMetricsSchema = z.object({
17
17
  day5Sold: z.number().nullable(),
18
18
  daysTracked: z.number().nullable(),
19
19
  });
20
- export const validationRunRequestSchema = z.object({
20
+ export const validationQueryContextSchema = z.object({
21
+ directQueryActive: z.boolean().nullable().optional(),
22
+ directSearchQuery: z.string().nullable().optional(),
23
+ resolvedSearchArtist: z.string().nullable().optional(),
24
+ resolvedSearchItem: z.string().nullable().optional(),
25
+ resolvedSearchEvent: z.string().nullable().optional(),
26
+ resolvedSearchLocation: z.string().nullable().optional(),
27
+ resolvedSearchQuery: z.string().nullable().optional(),
28
+ validationScope: z.string().nullable().optional(),
29
+ queryScope: z.string().nullable().optional(),
30
+ });
31
+ export const validationSourceContextSchema = z.object({
32
+ sourceType: z.enum(['item', 'event']).optional(),
33
+ hasItem: z.boolean().optional(),
34
+ hasEvent: z.boolean().optional(),
35
+ itemRecordId: z.string().nullable().optional(),
36
+ eventRecordId: z.string().nullable().optional(),
37
+ });
38
+ const validationItemSchema = z.object({
39
+ recordId: z.string().min(1).nullable(),
40
+ name: z.string(),
41
+ variation: z.array(z.string()),
42
+ itemType: z.array(z.string()),
43
+ releaseType: z.array(z.string()),
44
+ releaseDate: z.string().datetime({ offset: true }).nullable(),
45
+ releasePeriod: z.array(z.string()),
46
+ availability: z.array(z.string()),
47
+ wholesalePrice: z.number().nullable(),
48
+ supplierNames: z.array(z.string()),
49
+ canonicalArtists: z.array(z.string()),
50
+ relatedAlbums: z.array(z.string()),
51
+ });
52
+ export const validationRunRequestSchema = z
53
+ .object({
21
54
  validationId: z.string().min(1),
22
55
  runType: z.enum(['scheduled', 'manual']),
23
56
  cadence: trackingCadenceSchema,
24
57
  timestamp: z.string().datetime({ offset: true }),
25
- item: z.object({
26
- recordId: z.string().min(1),
27
- name: z.string().min(1),
28
- variation: z.array(z.string()),
29
- itemType: z.array(z.string()),
30
- releaseType: z.array(z.string()),
31
- releaseDate: z.string().datetime({ offset: true }).nullable(),
32
- releasePeriod: z.array(z.string()),
33
- availability: z.array(z.string()),
34
- wholesalePrice: z.number().nullable(),
35
- supplierNames: z.array(z.string()),
36
- canonicalArtists: z.array(z.string()),
37
- relatedAlbums: z.array(z.string()),
38
- }),
58
+ sourceContext: validationSourceContextSchema.optional(),
59
+ item: validationItemSchema,
39
60
  validation: z.object({
40
61
  validationType: z.string(),
41
62
  buyDecision: z.string(),
@@ -45,8 +66,31 @@ export const validationRunRequestSchema = z.object({
45
66
  artistTier: z.string(),
46
67
  initialBudget: z.number().nullable(),
47
68
  reserveBudget: z.number().nullable(),
69
+ queryContext: validationQueryContextSchema.optional(),
48
70
  currentMetrics: validationCurrentMetricsSchema,
49
71
  }),
72
+ })
73
+ .superRefine((value, ctx) => {
74
+ const sourceType = value.sourceContext?.sourceType ?? 'item';
75
+ const itemRecordId = value.item.recordId?.trim() ?? '';
76
+ const itemName = value.item.name.trim();
77
+ if (sourceType === 'event') {
78
+ return;
79
+ }
80
+ if (itemRecordId.length === 0) {
81
+ ctx.addIssue({
82
+ code: z.ZodIssueCode.custom,
83
+ path: ['item', 'recordId'],
84
+ message: 'Item-driven validations require item.recordId.',
85
+ });
86
+ }
87
+ if (itemName.length === 0) {
88
+ ctx.addIssue({
89
+ code: z.ZodIssueCode.custom,
90
+ path: ['item', 'name'],
91
+ message: 'Item-driven validations require item.name.',
92
+ });
93
+ }
50
94
  });
51
95
  export const validationWritesSchema = z.object({
52
96
  avgWatchersPerListing: z.number().nullable().optional(),
@@ -64,6 +108,9 @@ export const validationWritesSchema = z.object({
64
108
  day4Sold: z.number().nullable().optional(),
65
109
  day5Sold: z.number().nullable().optional(),
66
110
  daysTracked: z.number().nullable().optional(),
111
+ previousPobAvgPriceUsd: z.number().nullable().optional(),
112
+ previousPobSellThroughPct: z.number().nullable().optional(),
113
+ previousComebackFirstWeekSales: z.number().nullable().optional(),
67
114
  monitoringNotes: z.string().optional(),
68
115
  lastDataSnapshot: z.string().optional(),
69
116
  latestAiRecommendation: z.string().optional(),