ebay-mcp-remote-edition 3.3.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.
- package/README.md +24 -3
- package/build/validation/effective-context.js +77 -0
- package/build/validation/providers/query-utils.js +141 -18
- package/build/validation/providers/research.js +3 -1
- package/build/validation/providers/social.js +4 -1
- package/build/validation/providers/terapeak.js +1 -1
- package/build/validation/recommendation.js +11 -4
- package/build/validation/run-validation.js +24 -10
- package/build/validation/schemas.js +53 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -458,6 +458,7 @@ The hosted backend now includes a deployment-oriented validation pipeline for no
|
|
|
458
458
|
Current module layout:
|
|
459
459
|
|
|
460
460
|
- [`src/validation/types.ts`](src/validation/types.ts) — request/response contracts for validation runs, decision payloads, debug payloads, and provider signal types
|
|
461
|
+
- [`src/validation/effective-context.ts`](src/validation/effective-context.ts) — source-aware normalization layer that converts raw request payloads into a first-class effective validation context for item and event runs
|
|
461
462
|
- [`src/validation/run-validation.ts`](src/validation/run-validation.ts) — orchestration entrypoint that validates input, queries providers, merges signals, and returns writes/decision/debug output
|
|
462
463
|
- [`src/validation/recommendation.ts`](src/validation/recommendation.ts) — recommendation and automation decision logic
|
|
463
464
|
- [`src/validation/providers/ebay.ts`](src/validation/providers/ebay.ts) — live eBay browse-market snapshot provider using the server's existing user-scoped eBay API client
|
|
@@ -489,8 +490,26 @@ Operationally, validation works like this:
|
|
|
489
490
|
3. The route looks up the configured validation runner user ID for that environment.
|
|
490
491
|
4. The server loads that user's stored refresh-token-backed credentials from the existing hosted auth store.
|
|
491
492
|
5. The validation orchestrator calls all six provider domains and gathers browse/current-market, sold enrichment, Terapeak/research contract data, social support signals, chart stub output, and previous-comeback research output.
|
|
492
|
-
6. [`runValidation()`](src/validation/run-validation.ts
|
|
493
|
-
7.
|
|
493
|
+
6. Before provider execution, [`runValidation()`](src/validation/run-validation.ts) builds a normalized `effectiveContext` so downstream logic consumes a source-aware model (`item` or `event`) instead of relying on empty item placeholders.
|
|
494
|
+
7. [`runValidation()`](src/validation/run-validation.ts) deterministically merges the provider outputs into normalized field writes.
|
|
495
|
+
8. The response returns those writes, a conservative buy/track decision block, and provider debug metadata for downstream systems.
|
|
496
|
+
|
|
497
|
+
#### Effective validation context
|
|
498
|
+
|
|
499
|
+
Validation runs now normalize incoming request data into an internal effective context before provider query planning and recommendation logic execute.
|
|
500
|
+
|
|
501
|
+
- **Item-scope runs** normalize to an item-oriented context with the resolved artist, album/item phrase, location, and resolved search query.
|
|
502
|
+
- **Event-scope runs** normalize to an event-oriented context with `searchArtist`, `searchEvent`, `searchItem`, `searchLocation`, timing metadata, and a derived `effectiveSearchQuery` when no direct resolved query is present.
|
|
503
|
+
- Providers and recommendation logic consume that normalized context rather than reasoning about blank `item.recordId` or `item.name` fields.
|
|
504
|
+
- Debug output now exposes `effectiveSourceType`, `effectiveContextMode`, `effectiveSearchQuery`, `hasItem`, and `hasEvent` so operators can confirm whether an event run was normalized correctly.
|
|
505
|
+
|
|
506
|
+
The request schema also now accepts source-aware query-context fields for hosted validation runs:
|
|
507
|
+
|
|
508
|
+
- `resolvedSearchArtist`
|
|
509
|
+
- `resolvedSearchItem`
|
|
510
|
+
- `resolvedSearchEvent`
|
|
511
|
+
- `resolvedSearchLocation`
|
|
512
|
+
- `resolvedSearchQuery`
|
|
494
513
|
|
|
495
514
|
The validation contract is intentionally split between stable route orchestration and swappable providers. That is why the current sold-data source can be replaced later without changing downstream orchestration or the hosted route contract implemented in [`src/validation/run-validation.ts`](src/validation/run-validation.ts).
|
|
496
515
|
|
|
@@ -595,10 +614,11 @@ Current backend status:
|
|
|
595
614
|
- Social support signals are implemented in phase 1.
|
|
596
615
|
- Chart data remains a stub.
|
|
597
616
|
- Validation is currently an **admin-operated hosted backend workflow**, not an MCP tool surface.
|
|
617
|
+
- Event-scope validations are now handled as first-class normalized runs instead of as item-shaped requests with null item identity tolerated for compatibility.
|
|
598
618
|
|
|
599
619
|
Provider behavior:
|
|
600
620
|
|
|
601
|
-
- **Browse/eBay provider:** [`src/validation/providers/ebay.ts`](src/validation/providers/ebay.ts) uses the eBay Browse API plus shared query fallback logic from [`src/validation/providers/query-utils.ts`](src/validation/providers/query-utils.ts). It walks multiple query candidates, records the selected query and tier in debug output, and uses heuristic matching rather than a strict catalog identity join.
|
|
621
|
+
- **Browse/eBay provider:** [`src/validation/providers/ebay.ts`](src/validation/providers/ebay.ts) uses the eBay Browse API plus shared query fallback logic from [`src/validation/providers/query-utils.ts`](src/validation/providers/query-utils.ts). It walks multiple query candidates, records the selected query and tier in debug output, and uses heuristic matching rather than a strict catalog identity join. Event-driven runs now build those fallback queries from normalized event context instead of raw item title assumptions.
|
|
602
622
|
- **Browse debug semantics:** validation debug now keeps browse candidate generation, selected query/tier, browse-specific sample size, and per-candidate result counts separate from sold-provider result counts so operators can tell whether the browse layer contributed a field, fell back to a weaker query, or returned no usable match.
|
|
603
623
|
- **Sold provider:** [`src/validation/providers/ebay-sold.ts`](src/validation/providers/ebay-sold.ts) uses a temporary external sold-data source configured by `SOLD_ITEMS_API_URL` and `SOLD_ITEMS_API_KEY`. It uses the same query-fallback strategy as the browse provider and returns sold-price ranges, sample sold items, and recent sold-velocity buckets when available.
|
|
604
624
|
- **Terapeak / eBay research provider:** [`src/validation/providers/terapeak.ts`](src/validation/providers/terapeak.ts) is currently a stable placeholder contract. It already defines the query/debug shape and output fields used by orchestration, including `previousPobAvgPriceUsd` and `previousPobSellThroughPct`, but live authenticated Terapeak/eBay research retrieval is not implemented yet.
|
|
@@ -609,6 +629,7 @@ Provider behavior:
|
|
|
609
629
|
Recommendation behavior:
|
|
610
630
|
|
|
611
631
|
- [`src/validation/recommendation.ts`](src/validation/recommendation.ts) now accepts Terapeak and research inputs alongside browse, sold, social, and chart signals.
|
|
632
|
+
- Recommendation generation also consumes the normalized effective context so event runs can carry source-aware monitoring notes and avoid item-only assumptions when no usable item identity exists.
|
|
612
633
|
- The decisioning remains intentionally conservative: Terapeak and research data can improve monitoring notes and confidence context, but the system still avoids aggressive automatic buy-state changes from partial or proxy signals alone.
|
|
613
634
|
|
|
614
635
|
Known limitations in the current implementation:
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
function sanitizeText(value) {
|
|
2
|
+
if (typeof value !== 'string') {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
const trimmed = value.trim();
|
|
6
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
7
|
+
}
|
|
8
|
+
function buildCompactText(...parts) {
|
|
9
|
+
const compact = parts
|
|
10
|
+
.map((part) => sanitizeText(part))
|
|
11
|
+
.filter((part) => part !== null)
|
|
12
|
+
.join(' ')
|
|
13
|
+
.replace(/\s+/g, ' ')
|
|
14
|
+
.trim();
|
|
15
|
+
return compact.length > 0 ? compact : null;
|
|
16
|
+
}
|
|
17
|
+
function getQueryContext(request) {
|
|
18
|
+
return request.validation.queryContext;
|
|
19
|
+
}
|
|
20
|
+
function getFallbackArtist(request) {
|
|
21
|
+
return sanitizeText(request.item.canonicalArtists[0]);
|
|
22
|
+
}
|
|
23
|
+
function getFallbackAlbum(request) {
|
|
24
|
+
return sanitizeText(request.item.relatedAlbums[0]);
|
|
25
|
+
}
|
|
26
|
+
function getFallbackItem(request) {
|
|
27
|
+
return sanitizeText(request.item.name);
|
|
28
|
+
}
|
|
29
|
+
export function buildValidationEffectiveContext(request) {
|
|
30
|
+
const queryContext = getQueryContext(request);
|
|
31
|
+
const sourceType = request.sourceContext?.sourceType ?? 'item';
|
|
32
|
+
const searchArtist = sanitizeText(queryContext?.resolvedSearchArtist) ?? getFallbackArtist(request);
|
|
33
|
+
const searchAlbum = sanitizeText(getFallbackAlbum(request));
|
|
34
|
+
const searchItem = sanitizeText(queryContext?.resolvedSearchItem) ??
|
|
35
|
+
(sourceType === 'item' ? getFallbackItem(request) : null);
|
|
36
|
+
const searchEvent = sanitizeText(queryContext?.resolvedSearchEvent);
|
|
37
|
+
const searchLocation = sanitizeText(queryContext?.resolvedSearchLocation);
|
|
38
|
+
const resolvedSearchQuery = sanitizeText(queryContext?.resolvedSearchQuery);
|
|
39
|
+
const itemRecordId = sanitizeText(request.item.recordId);
|
|
40
|
+
const eventRecordId = sanitizeText(request.sourceContext?.eventRecordId);
|
|
41
|
+
const itemName = getFallbackItem(request);
|
|
42
|
+
const hasItem = sourceType === 'item'
|
|
43
|
+
? (request.sourceContext?.hasItem ?? itemRecordId !== null) || itemName !== null
|
|
44
|
+
: request.sourceContext?.hasItem === true || itemRecordId !== null;
|
|
45
|
+
const hasEvent = sourceType === 'event'
|
|
46
|
+
? true
|
|
47
|
+
: request.sourceContext?.hasEvent === true || searchEvent !== null || eventRecordId !== null;
|
|
48
|
+
const effectiveSearchQuery = resolvedSearchQuery ??
|
|
49
|
+
(sourceType === 'event'
|
|
50
|
+
? buildCompactText(searchArtist, searchEvent, searchItem, searchLocation)
|
|
51
|
+
: buildCompactText(searchArtist, searchAlbum ?? searchItem, searchLocation));
|
|
52
|
+
return {
|
|
53
|
+
sourceType,
|
|
54
|
+
mode: sourceType,
|
|
55
|
+
validationScope: sanitizeText(queryContext?.validationScope),
|
|
56
|
+
queryScope: sanitizeText(queryContext?.queryScope),
|
|
57
|
+
directQueryActive: queryContext?.directQueryActive === true,
|
|
58
|
+
resolvedSearchQuery,
|
|
59
|
+
effectiveSearchQuery,
|
|
60
|
+
searchArtist,
|
|
61
|
+
searchAlbum,
|
|
62
|
+
searchItem,
|
|
63
|
+
searchEvent,
|
|
64
|
+
searchLocation,
|
|
65
|
+
hasItem,
|
|
66
|
+
hasEvent,
|
|
67
|
+
itemRecordId,
|
|
68
|
+
eventRecordId,
|
|
69
|
+
itemName,
|
|
70
|
+
eventDate: sanitizeText(request.item.releaseDate),
|
|
71
|
+
dDay: request.validation.dDay,
|
|
72
|
+
requestTimestamp: request.timestamp,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function getValidationEffectiveContext(request) {
|
|
76
|
+
return request.effectiveContext ?? buildValidationEffectiveContext(request);
|
|
77
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getValidationEffectiveContext } from '../effective-context.js';
|
|
1
2
|
const NOISY_VERSION_PATTERNS = [
|
|
2
3
|
/\((?:weverse albums?|weverse|digipack|platform|photobook|poca|poca album|kit)\s+ver\.?\)/gi,
|
|
3
4
|
/\((?:weverse albums?|weverse|digipack|platform|photobook|poca|poca album|kit)\s+version\)/gi,
|
|
@@ -107,14 +108,28 @@ function dedupeQueries(candidates) {
|
|
|
107
108
|
return result;
|
|
108
109
|
}
|
|
109
110
|
function getPrimaryArtist(request) {
|
|
110
|
-
return request.
|
|
111
|
+
return (getValidationEffectiveContext(request).searchArtist ??
|
|
112
|
+
request.item.canonicalArtists[0]?.trim() ??
|
|
113
|
+
'');
|
|
111
114
|
}
|
|
112
115
|
export function getPrimaryAlbumPhrase(request) {
|
|
113
|
-
const
|
|
116
|
+
const effectiveContext = getValidationEffectiveContext(request);
|
|
117
|
+
if (effectiveContext.sourceType === 'event') {
|
|
118
|
+
const eventPhrase = sanitizeQueryCandidate(effectiveContext.searchEvent ?? '');
|
|
119
|
+
if (eventPhrase && extractSemanticTokens(eventPhrase).length > 0) {
|
|
120
|
+
return eventPhrase;
|
|
121
|
+
}
|
|
122
|
+
const itemPhrase = sanitizeQueryCandidate(effectiveContext.searchItem ?? '');
|
|
123
|
+
if (itemPhrase && extractSemanticTokens(itemPhrase).length > 0) {
|
|
124
|
+
return itemPhrase;
|
|
125
|
+
}
|
|
126
|
+
return sanitizeQueryCandidate(effectiveContext.searchLocation ?? '');
|
|
127
|
+
}
|
|
128
|
+
const relatedAlbum = sanitizeQueryCandidate(effectiveContext.searchAlbum ?? request.item.relatedAlbums[0]?.trim() ?? '');
|
|
114
129
|
if (relatedAlbum && extractSemanticTokens(relatedAlbum).length > 0) {
|
|
115
130
|
return relatedAlbum;
|
|
116
131
|
}
|
|
117
|
-
const simplifiedTitle = simplifyItemTitle(request.item.name);
|
|
132
|
+
const simplifiedTitle = simplifyItemTitle(effectiveContext.searchItem ?? request.item.name);
|
|
118
133
|
const withoutArtist = stripArtistsFromText(simplifiedTitle, request.item.canonicalArtists);
|
|
119
134
|
const cleanedWithoutArtist = sanitizeQueryCandidate(removeBracketedContent(withoutArtist));
|
|
120
135
|
if (extractSemanticTokens(cleanedWithoutArtist).length > 0) {
|
|
@@ -124,6 +139,13 @@ export function getPrimaryAlbumPhrase(request) {
|
|
|
124
139
|
return extractSemanticTokens(cleanedTitle).length > 0 ? cleanedTitle : relatedAlbum;
|
|
125
140
|
}
|
|
126
141
|
export function getPrimarySocialAlbumPhrase(request) {
|
|
142
|
+
const effectiveContext = getValidationEffectiveContext(request);
|
|
143
|
+
if (effectiveContext.sourceType === 'event') {
|
|
144
|
+
const eventPhrase = buildConversationAlbumPhrase(buildCompactPhrase(effectiveContext.searchEvent ?? '', effectiveContext.searchItem ?? '', effectiveContext.searchLocation ?? ''));
|
|
145
|
+
if (extractSemanticTokens(eventPhrase).length > 0) {
|
|
146
|
+
return eventPhrase;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
127
149
|
for (const relatedAlbum of request.item.relatedAlbums) {
|
|
128
150
|
const sanitizedRelatedAlbum = sanitizeQueryCandidate(relatedAlbum?.trim() ?? '');
|
|
129
151
|
if (!sanitizedRelatedAlbum) {
|
|
@@ -204,6 +226,18 @@ function normalizeDescriptorPhrase(value) {
|
|
|
204
226
|
.replace(/\b(?:sealed|new|official)\b/gi, ' '));
|
|
205
227
|
}
|
|
206
228
|
function collectDescriptorPhrases(request, options) {
|
|
229
|
+
const effectiveContext = getValidationEffectiveContext(request);
|
|
230
|
+
if (effectiveContext.sourceType === 'event') {
|
|
231
|
+
const eventDescriptors = [
|
|
232
|
+
effectiveContext.searchItem,
|
|
233
|
+
effectiveContext.searchLocation,
|
|
234
|
+
options.includeValidationType ? request.validation.validationType : null,
|
|
235
|
+
]
|
|
236
|
+
.map((value) => normalizeDescriptorPhrase(value ?? ''))
|
|
237
|
+
.filter(Boolean)
|
|
238
|
+
.filter((value) => !GENERIC_DESCRIPTOR_PATTERN.test(value.toLowerCase()));
|
|
239
|
+
return dedupeQueries(eventDescriptors).slice(0, 3);
|
|
240
|
+
}
|
|
207
241
|
const rawValues = [
|
|
208
242
|
...request.item.variation,
|
|
209
243
|
...request.item.itemType,
|
|
@@ -263,6 +297,93 @@ export function getQueryContext(request) {
|
|
|
263
297
|
export function getResolvedSearchQuery(request) {
|
|
264
298
|
return sanitizeQueryContextValue(getQueryContext(request)?.resolvedSearchQuery);
|
|
265
299
|
}
|
|
300
|
+
function getUsableResolvedSearchQuery(request) {
|
|
301
|
+
const resolvedSearchQuery = getResolvedSearchQuery(request);
|
|
302
|
+
return resolvedSearchQuery && !isRejectedResolvedQuery(resolvedSearchQuery)
|
|
303
|
+
? resolvedSearchQuery
|
|
304
|
+
: null;
|
|
305
|
+
}
|
|
306
|
+
function getNormalizedQueryScope(request) {
|
|
307
|
+
return sanitizeQueryContextValue(getQueryContext(request)?.queryScope)?.toLowerCase() ?? null;
|
|
308
|
+
}
|
|
309
|
+
function resolveDeclaredQueryScope(request) {
|
|
310
|
+
const normalizedQueryScope = getNormalizedQueryScope(request);
|
|
311
|
+
if (!normalizedQueryScope) {
|
|
312
|
+
return 'unknown';
|
|
313
|
+
}
|
|
314
|
+
if (normalizedQueryScope.includes('direct query')) {
|
|
315
|
+
return 'direct_query';
|
|
316
|
+
}
|
|
317
|
+
const hasArtist = normalizedQueryScope.includes('artist');
|
|
318
|
+
const hasItem = normalizedQueryScope.includes('item');
|
|
319
|
+
const hasAlbum = normalizedQueryScope.includes('album');
|
|
320
|
+
const hasEvent = normalizedQueryScope.includes('event');
|
|
321
|
+
const hasLocation = normalizedQueryScope.includes('city') ||
|
|
322
|
+
normalizedQueryScope.includes('country') ||
|
|
323
|
+
normalizedQueryScope.includes('state') ||
|
|
324
|
+
normalizedQueryScope.includes('province') ||
|
|
325
|
+
normalizedQueryScope.includes('location');
|
|
326
|
+
if (hasArtist && hasItem && hasLocation) {
|
|
327
|
+
return 'artist_item_location';
|
|
328
|
+
}
|
|
329
|
+
if (hasArtist && hasLocation) {
|
|
330
|
+
return 'artist_location';
|
|
331
|
+
}
|
|
332
|
+
if (hasArtist && hasEvent) {
|
|
333
|
+
return 'artist_event';
|
|
334
|
+
}
|
|
335
|
+
if (hasArtist && hasAlbum) {
|
|
336
|
+
return 'artist_album';
|
|
337
|
+
}
|
|
338
|
+
if (hasArtist && hasItem) {
|
|
339
|
+
return 'artist_item';
|
|
340
|
+
}
|
|
341
|
+
if (normalizedQueryScope === 'artist only' || normalizedQueryScope === 'artist') {
|
|
342
|
+
return 'artist_only';
|
|
343
|
+
}
|
|
344
|
+
return 'unknown';
|
|
345
|
+
}
|
|
346
|
+
function hasExclusiveDirectQueryOverride(request) {
|
|
347
|
+
const queryContext = getQueryContext(request);
|
|
348
|
+
return (queryContext?.directQueryActive === true && getNormalizedQueryScope(request) === 'direct query');
|
|
349
|
+
}
|
|
350
|
+
function finalizeLooseQueryPlan(candidates) {
|
|
351
|
+
return dedupeQueryPlan(candidates).filter((candidate) => isValidConversationQuery(candidate.query));
|
|
352
|
+
}
|
|
353
|
+
function buildArtistOnlyCommerceFallbackPlan(request) {
|
|
354
|
+
return finalizeLooseQueryPlan([
|
|
355
|
+
{
|
|
356
|
+
family: 'artist_only_fallback',
|
|
357
|
+
query: getPrimaryArtist(request),
|
|
358
|
+
},
|
|
359
|
+
]);
|
|
360
|
+
}
|
|
361
|
+
function buildArtistOnlySocialFallbackPlan(request) {
|
|
362
|
+
return finalizeLooseQueryPlan([
|
|
363
|
+
{
|
|
364
|
+
family: 'artist_only_fallback',
|
|
365
|
+
query: normalizeSocialSearchPhrase(getPrimaryArtist(request)),
|
|
366
|
+
},
|
|
367
|
+
]);
|
|
368
|
+
}
|
|
369
|
+
function constrainFallbackPlanForScope(request, fallbackPlan, scopeSpecificFallbackPlan) {
|
|
370
|
+
if (getUsableResolvedSearchQuery(request) === null) {
|
|
371
|
+
return fallbackPlan;
|
|
372
|
+
}
|
|
373
|
+
switch (resolveDeclaredQueryScope(request)) {
|
|
374
|
+
case 'artist_only':
|
|
375
|
+
return scopeSpecificFallbackPlan;
|
|
376
|
+
case 'artist_item':
|
|
377
|
+
case 'artist_album':
|
|
378
|
+
case 'unknown':
|
|
379
|
+
return fallbackPlan;
|
|
380
|
+
case 'artist_event':
|
|
381
|
+
case 'artist_location':
|
|
382
|
+
case 'artist_item_location':
|
|
383
|
+
case 'direct_query':
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
266
387
|
export function buildProviderQueryResolutionDebug(request, queryContextUsed) {
|
|
267
388
|
const queryContext = getQueryContext(request);
|
|
268
389
|
const resolvedSearchQuery = getResolvedSearchQuery(request);
|
|
@@ -275,15 +396,14 @@ export function buildProviderQueryResolutionDebug(request, queryContextUsed) {
|
|
|
275
396
|
};
|
|
276
397
|
}
|
|
277
398
|
export function prependResolvedQueryCandidate(request, fallbackPlan) {
|
|
278
|
-
const
|
|
279
|
-
const usableResolvedQuery = resolvedSearchQuery && !isRejectedResolvedQuery(resolvedSearchQuery)
|
|
280
|
-
? resolvedSearchQuery
|
|
281
|
-
: null;
|
|
399
|
+
const usableResolvedQuery = getUsableResolvedSearchQuery(request);
|
|
282
400
|
const queryPlan = usableResolvedQuery
|
|
283
|
-
?
|
|
284
|
-
{ family: 'resolved_query_context', query: usableResolvedQuery }
|
|
285
|
-
|
|
286
|
-
|
|
401
|
+
? hasExclusiveDirectQueryOverride(request)
|
|
402
|
+
? [{ family: 'resolved_query_context', query: usableResolvedQuery }]
|
|
403
|
+
: dedupeQueryPlan([
|
|
404
|
+
{ family: 'resolved_query_context', query: usableResolvedQuery },
|
|
405
|
+
...fallbackPlan,
|
|
406
|
+
])
|
|
287
407
|
: fallbackPlan;
|
|
288
408
|
return {
|
|
289
409
|
queryPlan,
|
|
@@ -308,9 +428,12 @@ function finalizeQueryPlan(candidates, primaryArtist, albumPhrase) {
|
|
|
308
428
|
return dedupeQueryPlan(candidates).filter((candidate) => isValidCandidate(candidate.query, primaryArtist, albumPhrase));
|
|
309
429
|
}
|
|
310
430
|
function buildCorePhrases(request) {
|
|
431
|
+
const effectiveContext = getValidationEffectiveContext(request);
|
|
311
432
|
const primaryArtist = getPrimaryArtist(request);
|
|
312
433
|
const albumPhrase = getPrimaryAlbumPhrase(request);
|
|
313
|
-
const simplifiedTitle =
|
|
434
|
+
const simplifiedTitle = effectiveContext.sourceType === 'event'
|
|
435
|
+
? sanitizeQueryCandidate(buildCompactPhrase(primaryArtist, effectiveContext.searchEvent ?? '', effectiveContext.searchItem ?? '', effectiveContext.searchLocation ?? ''))
|
|
436
|
+
: sanitizeQueryCandidate(removeBracketedContent(simplifyItemTitle(request.item.name)));
|
|
314
437
|
const artistAlbumPhrase = ensureArtistRetention(buildCompactPhrase(primaryArtist, albumPhrase), primaryArtist);
|
|
315
438
|
const titleWithArtist = ensureArtistRetention(simplifiedTitle, primaryArtist);
|
|
316
439
|
return {
|
|
@@ -344,7 +467,7 @@ export function buildBrowseQueryPlan(request) {
|
|
|
344
467
|
], primaryArtist, albumPhrase);
|
|
345
468
|
}
|
|
346
469
|
export function buildResolvedBrowseQueryPlan(request) {
|
|
347
|
-
return prependResolvedQueryCandidate(request, buildBrowseQueryPlan(request));
|
|
470
|
+
return prependResolvedQueryCandidate(request, constrainFallbackPlanForScope(request, buildBrowseQueryPlan(request), buildArtistOnlyCommerceFallbackPlan(request)));
|
|
348
471
|
}
|
|
349
472
|
export function buildBrowseQueryCandidates(request) {
|
|
350
473
|
return buildBrowseQueryPlan(request).map((candidate) => candidate.query);
|
|
@@ -372,7 +495,7 @@ export function buildSoldQueryPlan(request) {
|
|
|
372
495
|
], primaryArtist, albumPhrase);
|
|
373
496
|
}
|
|
374
497
|
export function buildResolvedSoldQueryPlan(request) {
|
|
375
|
-
return prependResolvedQueryCandidate(request, buildSoldQueryPlan(request));
|
|
498
|
+
return prependResolvedQueryCandidate(request, constrainFallbackPlanForScope(request, buildSoldQueryPlan(request), buildArtistOnlyCommerceFallbackPlan(request)));
|
|
376
499
|
}
|
|
377
500
|
export function buildSoldQueryCandidates(request) {
|
|
378
501
|
return buildSoldQueryPlan(request).map((candidate) => candidate.query);
|
|
@@ -398,7 +521,7 @@ export function buildTwitterQueryPlan(request) {
|
|
|
398
521
|
return finalizeConversationQueryPlan(candidates);
|
|
399
522
|
}
|
|
400
523
|
export function buildResolvedTwitterQueryPlan(request) {
|
|
401
|
-
return prependResolvedQueryCandidate(request, buildTwitterQueryPlan(request));
|
|
524
|
+
return prependResolvedQueryCandidate(request, constrainFallbackPlanForScope(request, buildTwitterQueryPlan(request), buildArtistOnlySocialFallbackPlan(request)));
|
|
402
525
|
}
|
|
403
526
|
export function buildTwitterQueryCandidates(request) {
|
|
404
527
|
return buildTwitterQueryPlan(request).map((candidate) => candidate.query);
|
|
@@ -422,7 +545,7 @@ export function buildYouTubeQueryPlan(request) {
|
|
|
422
545
|
: [{ family: 'artist_only_media_fallback', query: compactArtist }]);
|
|
423
546
|
}
|
|
424
547
|
export function buildResolvedYouTubeQueryPlan(request) {
|
|
425
|
-
return prependResolvedQueryCandidate(request, buildYouTubeQueryPlan(request));
|
|
548
|
+
return prependResolvedQueryCandidate(request, constrainFallbackPlanForScope(request, buildYouTubeQueryPlan(request), buildArtistOnlySocialFallbackPlan(request)));
|
|
426
549
|
}
|
|
427
550
|
export function buildYouTubeQueryCandidates(request) {
|
|
428
551
|
return buildYouTubeQueryPlan(request).map((candidate) => candidate.query);
|
|
@@ -441,7 +564,7 @@ export function buildRedditQueryPlan(request) {
|
|
|
441
564
|
], primaryArtist, compactAlbum);
|
|
442
565
|
}
|
|
443
566
|
export function buildResolvedRedditQueryPlan(request) {
|
|
444
|
-
return prependResolvedQueryCandidate(request, buildRedditQueryPlan(request));
|
|
567
|
+
return prependResolvedQueryCandidate(request, constrainFallbackPlanForScope(request, buildRedditQueryPlan(request), buildArtistOnlySocialFallbackPlan(request)));
|
|
445
568
|
}
|
|
446
569
|
export function buildRedditQueryCandidates(request) {
|
|
447
570
|
return buildRedditQueryPlan(request).map((candidate) => candidate.query);
|
|
@@ -450,5 +573,5 @@ export function buildValidationQueryCandidates(request) {
|
|
|
450
573
|
return buildSoldQueryCandidates(request);
|
|
451
574
|
}
|
|
452
575
|
export function buildResolvedValidationQueryPlan(request) {
|
|
453
|
-
return prependResolvedQueryCandidate(request, buildSoldQueryPlan(request));
|
|
576
|
+
return prependResolvedQueryCandidate(request, constrainFallbackPlanForScope(request, buildSoldQueryPlan(request), buildArtistOnlyCommerceFallbackPlan(request)));
|
|
454
577
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { getValidationEffectiveContext } from '../effective-context.js';
|
|
1
2
|
export async function getPreviousComebackResearchSignals(request) {
|
|
2
3
|
await Promise.resolve();
|
|
3
4
|
const hasPerplexityKey = (process.env.PERPLEXITY_API_KEY ?? '').trim().length > 0;
|
|
4
|
-
const
|
|
5
|
+
const effectiveContext = getValidationEffectiveContext(request);
|
|
6
|
+
const primaryAlbum = effectiveContext.searchAlbum ?? effectiveContext.searchEvent ?? effectiveContext.searchItem;
|
|
5
7
|
return {
|
|
6
8
|
previousAlbumTitle: null,
|
|
7
9
|
previousComebackFirstWeekSales: null,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import { buildConversationAlbumPhrase, buildResolvedRedditQueryPlan, buildResolvedTwitterQueryPlan, buildResolvedYouTubeQueryPlan, extractSemanticTokens, getPrimaryAlbumPhrase, getPrimarySocialAlbumPhrase, normalizeWhitespace, } from './query-utils.js';
|
|
3
|
+
import { getValidationEffectiveContext } from '../effective-context.js';
|
|
3
4
|
const buildResolvedTwitterQueryPlanSafe = buildResolvedTwitterQueryPlan;
|
|
4
5
|
const buildResolvedYouTubeQueryPlanSafe = buildResolvedYouTubeQueryPlan;
|
|
5
6
|
const buildResolvedRedditQueryPlanSafe = buildResolvedRedditQueryPlan;
|
|
@@ -20,7 +21,9 @@ const YOUTUBE_OFFICIAL_CHANNEL_PATTERN = /\bofficial\b|\btopic\b/;
|
|
|
20
21
|
const YOUTUBE_BRANDED_CHANNEL_PATTERN = /entertainment|music|records|labels?|media|studio|vevo/;
|
|
21
22
|
const YOUTUBE_DEMOTED_CHANNEL_PATTERN = /shop\b|store\b|merch|reseller|resale|unboxing|fan\b|collector|trading|market/;
|
|
22
23
|
function getPrimaryArtist(request) {
|
|
23
|
-
return request.
|
|
24
|
+
return (getValidationEffectiveContext(request).searchArtist ??
|
|
25
|
+
request.item.canonicalArtists[0]?.trim() ??
|
|
26
|
+
'');
|
|
24
27
|
}
|
|
25
28
|
function buildTwitterCountsUrl(query) {
|
|
26
29
|
return `https://api.x.com/2/tweets/counts/recent?query=${encodeURIComponent(query)}`;
|
|
@@ -4,7 +4,7 @@ export async function getTerapeakValidationSignals(_api, request) {
|
|
|
4
4
|
const { queryPlan, queryResolution } = buildResolvedValidationQueryPlan(request);
|
|
5
5
|
const queryCandidates = queryPlan.map((candidate) => candidate.query);
|
|
6
6
|
const currentQuery = queryCandidates[0] ?? null;
|
|
7
|
-
const previousPobQuery = queryCandidates[1] ??
|
|
7
|
+
const previousPobQuery = queryCandidates[1] ?? null;
|
|
8
8
|
return {
|
|
9
9
|
avgWatchersPerListing: null,
|
|
10
10
|
preOrderListingsCount: null,
|
|
@@ -17,10 +17,18 @@ export function buildValidationRecommendation(request, signals) {
|
|
|
17
17
|
signals.sold.soldMedianPriceUsd ??
|
|
18
18
|
signals.ebay.marketPriceUsd;
|
|
19
19
|
const preorderListingsCount = signals.terapeak.preOrderListingsCount ?? signals.ebay.preOrderListingsCount;
|
|
20
|
-
const wholesale = request.item.wholesalePrice;
|
|
20
|
+
const wholesale = signals.effectiveContext.hasItem ? request.item.wholesalePrice : null;
|
|
21
21
|
const marginRatio = marketPrice !== null && wholesale !== null && wholesale > 0
|
|
22
22
|
? (marketPrice - wholesale) / wholesale
|
|
23
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');
|
|
24
32
|
const recentSoldCount = [
|
|
25
33
|
signals.sold.soldVelocity.day1Sold,
|
|
26
34
|
signals.sold.soldVelocity.day2Sold,
|
|
@@ -28,13 +36,12 @@ export function buildValidationRecommendation(request, signals) {
|
|
|
28
36
|
].reduce((sum, value) => sum + (value ?? 0), 0);
|
|
29
37
|
let latestAiRecommendation = 'Continue watching until stronger market signal appears.';
|
|
30
38
|
let latestAiConfidence = 'Medium';
|
|
31
|
-
let monitoringNotes =
|
|
39
|
+
let monitoringNotes = `Baseline recommendation generated from current ${signals.effectiveContext.mode} validation state for ${subjectLabel}.`;
|
|
32
40
|
if (!shouldAutoTrack) {
|
|
33
41
|
latestAiRecommendation =
|
|
34
42
|
'Automatic tracking paused because the validation is no longer in a watchable state.';
|
|
35
43
|
latestAiConfidence = 'High';
|
|
36
|
-
monitoringNotes =
|
|
37
|
-
'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}.`;
|
|
38
45
|
}
|
|
39
46
|
else if (marginRatio !== null &&
|
|
40
47
|
marginRatio >= 1 &&
|
|
@@ -7,6 +7,7 @@ import { getChartValidationSignals } from './providers/chart.js';
|
|
|
7
7
|
import { getPreviousComebackResearchSignals } from './providers/research.js';
|
|
8
8
|
import { buildProviderQueryResolutionDebug } from './providers/query-utils.js';
|
|
9
9
|
import { buildValidationRecommendation } from './recommendation.js';
|
|
10
|
+
import { buildValidationEffectiveContext } from './effective-context.js';
|
|
10
11
|
function addMinutes(timestamp, minutes) {
|
|
11
12
|
return new Date(new Date(timestamp).getTime() + minutes * 60 * 1000).toISOString();
|
|
12
13
|
}
|
|
@@ -210,12 +211,17 @@ export async function runValidation(api, input) {
|
|
|
210
211
|
};
|
|
211
212
|
}
|
|
212
213
|
try {
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
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);
|
|
219
225
|
const mergedAvgWatchers = terapeak.avgWatchersPerListing ?? ebay.avgWatchersPerListing;
|
|
220
226
|
const mergedPreorderListings = terapeak.preOrderListingsCount ?? ebay.preOrderListingsCount;
|
|
221
227
|
const marketPriceUsd = terapeak.marketPriceUsd ?? sold.soldMedianPriceUsd ?? ebay.marketPriceUsd;
|
|
@@ -229,16 +235,17 @@ export async function runValidation(api, input) {
|
|
|
229
235
|
day5Sold: sold.soldVelocity.day5Sold ?? ebay.soldVelocity.day5Sold,
|
|
230
236
|
daysTracked: sold.soldVelocity.daysTracked ?? ebay.soldVelocity.daysTracked,
|
|
231
237
|
};
|
|
232
|
-
const recommendation = buildValidationRecommendation(
|
|
238
|
+
const recommendation = buildValidationRecommendation(effectiveRequest, {
|
|
233
239
|
ebay,
|
|
234
240
|
sold,
|
|
235
241
|
terapeak,
|
|
236
242
|
social,
|
|
237
243
|
chart,
|
|
238
244
|
research,
|
|
245
|
+
effectiveContext,
|
|
239
246
|
});
|
|
240
|
-
const requestQueryResolution = buildProviderQueryResolutionDebug(
|
|
241
|
-
const mergedSignals = { ebay, sold, terapeak, social, chart, research };
|
|
247
|
+
const requestQueryResolution = buildProviderQueryResolutionDebug(effectiveRequest, Boolean(ebay.queryResolution?.queryContextUsed));
|
|
248
|
+
const mergedSignals = { effectiveContext, ebay, sold, terapeak, social, chart, research };
|
|
242
249
|
const socialWrites = {
|
|
243
250
|
...(social.twitterTrending !== null ? { twitterTrending: social.twitterTrending } : {}),
|
|
244
251
|
...(social.youtubeViews24hMillions !== null
|
|
@@ -340,6 +347,13 @@ export async function runValidation(api, input) {
|
|
|
340
347
|
nextCheckAt: recommendation.nextCheckAt,
|
|
341
348
|
},
|
|
342
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,
|
|
343
357
|
ebayQuery: ebay.ebayQuery,
|
|
344
358
|
soldQuery: sold.query,
|
|
345
359
|
queryCandidates: {
|
|
@@ -352,7 +366,7 @@ export async function runValidation(api, input) {
|
|
|
352
366
|
omittedOptionalWrites,
|
|
353
367
|
writeResolution,
|
|
354
368
|
sourceSet: ['ebay', 'sold', 'terapeak', 'social', 'chart', 'research'],
|
|
355
|
-
providers: buildProviderDebug(
|
|
369
|
+
providers: buildProviderDebug(effectiveRequest, ebay, sold, terapeak, social, chart, research),
|
|
356
370
|
queryContextUsed: requestQueryResolution.queryContextUsed,
|
|
357
371
|
querySource: requestQueryResolution.querySource,
|
|
358
372
|
resolvedSearchQuery: requestQueryResolution.resolvedSearchQuery,
|
|
@@ -18,29 +18,45 @@ export const validationCurrentMetricsSchema = z.object({
|
|
|
18
18
|
daysTracked: z.number().nullable(),
|
|
19
19
|
});
|
|
20
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(),
|
|
21
27
|
resolvedSearchQuery: z.string().nullable().optional(),
|
|
22
28
|
validationScope: z.string().nullable().optional(),
|
|
23
29
|
queryScope: z.string().nullable().optional(),
|
|
24
30
|
});
|
|
25
|
-
export const
|
|
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({
|
|
26
54
|
validationId: z.string().min(1),
|
|
27
55
|
runType: z.enum(['scheduled', 'manual']),
|
|
28
56
|
cadence: trackingCadenceSchema,
|
|
29
57
|
timestamp: z.string().datetime({ offset: true }),
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
name: z.string().min(1),
|
|
33
|
-
variation: z.array(z.string()),
|
|
34
|
-
itemType: z.array(z.string()),
|
|
35
|
-
releaseType: z.array(z.string()),
|
|
36
|
-
releaseDate: z.string().datetime({ offset: true }).nullable(),
|
|
37
|
-
releasePeriod: z.array(z.string()),
|
|
38
|
-
availability: z.array(z.string()),
|
|
39
|
-
wholesalePrice: z.number().nullable(),
|
|
40
|
-
supplierNames: z.array(z.string()),
|
|
41
|
-
canonicalArtists: z.array(z.string()),
|
|
42
|
-
relatedAlbums: z.array(z.string()),
|
|
43
|
-
}),
|
|
58
|
+
sourceContext: validationSourceContextSchema.optional(),
|
|
59
|
+
item: validationItemSchema,
|
|
44
60
|
validation: z.object({
|
|
45
61
|
validationType: z.string(),
|
|
46
62
|
buyDecision: z.string(),
|
|
@@ -53,6 +69,28 @@ export const validationRunRequestSchema = z.object({
|
|
|
53
69
|
queryContext: validationQueryContextSchema.optional(),
|
|
54
70
|
currentMetrics: validationCurrentMetricsSchema,
|
|
55
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
|
+
}
|
|
56
94
|
});
|
|
57
95
|
export const validationWritesSchema = z.object({
|
|
58
96
|
avgWatchersPerListing: z.number().nullable().optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ebay-mcp-remote-edition",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"description": "Remote + Local MCP server for eBay APIs - provides access to eBay developer functionality through MCP (Model Context Protocol)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|