ebay-mcp-remote-edition 4.0.0 → 4.1.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/README.md
CHANGED
|
@@ -626,7 +626,7 @@ Provider behavior:
|
|
|
626
626
|
- **Authenticated research session source:** [`src/validation/providers/ebay-research.ts`](src/validation/providers/ebay-research.ts) can source eBay Research authentication from cookie JSON, persisted KV-backed session state, Playwright storage state, or a browser profile directory. Parsed ACTIVE and SOLD tab responses are cached, but automatically invalidated when the authenticated cookie fingerprint changes.
|
|
627
627
|
- **Social provider:** [`src/validation/providers/social.ts`](src/validation/providers/social.ts) supports phase-1 Twitter/X recent activity, YouTube average-daily-views proxy data exposed through the `youtubeViews24hMillions` field, and Reddit recent post counts. These signals degrade gracefully on provider/API failure and are used as supportive indicators rather than authoritative demand truth.
|
|
628
628
|
- **Chart provider:** [`src/validation/providers/chart.ts`](src/validation/providers/chart.ts) is still a stub and does not currently contribute chart-based metrics.
|
|
629
|
-
- **Previous comeback research provider:** [`src/validation/providers/research.ts`](src/validation/providers/research.ts)
|
|
629
|
+
- **Previous comeback research provider:** [`src/validation/providers/research.ts`](src/validation/providers/research.ts) now performs Perplexity-backed historical research when `PERPLEXITY_API_KEY` is configured. It attempts to resolve the prior comeback, normalize previous first-week sales when support exists, assign a `perplexityHistoricalContextScore`, generate concise `historicalContextNotes`, and emit debug diagnostics covering the research query, citations/snippets, resolved prior release, confidence, and score reasoning.
|
|
630
630
|
|
|
631
631
|
Recommendation behavior:
|
|
632
632
|
|
|
@@ -641,7 +641,7 @@ Known limitations in the current implementation:
|
|
|
641
641
|
- If those sold-data variables are missing, validation still runs but sold enrichment degrades to an unavailable/error state rather than providing full historical-sales signals.
|
|
642
642
|
- The sold-data provider is temporary and intended to be replaced by an internal implementation later.
|
|
643
643
|
- Authenticated eBay Research requires a valid session source such as `EBAY_RESEARCH_COOKIES_JSON`, a persisted KV session, Playwright storage state, or a browser profile directory; without one, the provider degrades to diagnostic-only output.
|
|
644
|
-
- The previous-comeback research provider
|
|
644
|
+
- The previous-comeback research provider depends on grounded external research and therefore degrades to low-confidence notes with a zero historical score when `PERPLEXITY_API_KEY` is missing, the response cannot be normalized, or reliable evidence is not found.
|
|
645
645
|
- The browse provider still relies on heuristic query selection and fallback matching.
|
|
646
646
|
- The YouTube-backed `youtubeViews24hMillions` field is currently an **average daily views proxy**, not a true trailing 24-hour delta.
|
|
647
647
|
- Social signals are supportive/proxy data only and should not be presented as decisive automated buy logic.
|
|
@@ -1,16 +1,497 @@
|
|
|
1
1
|
import { getValidationEffectiveContext } from '../effective-context.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
function sanitizeText(value) {
|
|
3
|
+
if (typeof value !== 'string') {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
8
|
+
}
|
|
9
|
+
function uniqueStrings(values, max = 6) {
|
|
10
|
+
const normalized = values
|
|
11
|
+
.map((value) => sanitizeText(value))
|
|
12
|
+
.filter((value) => value !== null);
|
|
13
|
+
return Array.from(new Set(normalized)).slice(0, max);
|
|
14
|
+
}
|
|
15
|
+
function clamp(value, min, max) {
|
|
16
|
+
return Math.min(max, Math.max(min, value));
|
|
17
|
+
}
|
|
18
|
+
function truncateText(value, maxLength) {
|
|
19
|
+
return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1).trimEnd()}…`;
|
|
20
|
+
}
|
|
21
|
+
function detectResearchSubtype(request) {
|
|
5
22
|
const effectiveContext = getValidationEffectiveContext(request);
|
|
6
|
-
|
|
23
|
+
if (effectiveContext.sourceType === 'event') {
|
|
24
|
+
return 'event';
|
|
25
|
+
}
|
|
26
|
+
const combined = [
|
|
27
|
+
request.validation.validationType,
|
|
28
|
+
request.validation.queryContext?.validationScope,
|
|
29
|
+
request.validation.queryContext?.queryScope,
|
|
30
|
+
]
|
|
31
|
+
.map((value) => sanitizeText(value)?.toLowerCase() ?? '')
|
|
32
|
+
.join(' ');
|
|
33
|
+
if (/\bpob\b|benefit|photocard/.test(combined)) {
|
|
34
|
+
return 'pob';
|
|
35
|
+
}
|
|
36
|
+
if (/pre\s*order|preorder/.test(combined)) {
|
|
37
|
+
return 'preorder';
|
|
38
|
+
}
|
|
39
|
+
if (combined.includes('album')) {
|
|
40
|
+
return 'album';
|
|
41
|
+
}
|
|
42
|
+
return 'general';
|
|
43
|
+
}
|
|
44
|
+
function buildResearchQuery(request) {
|
|
45
|
+
const effectiveContext = getValidationEffectiveContext(request);
|
|
46
|
+
const parts = effectiveContext.sourceType === 'event'
|
|
47
|
+
? [
|
|
48
|
+
effectiveContext.searchArtist,
|
|
49
|
+
effectiveContext.searchEvent,
|
|
50
|
+
effectiveContext.searchItem,
|
|
51
|
+
effectiveContext.searchLocation,
|
|
52
|
+
]
|
|
53
|
+
: [effectiveContext.searchArtist, effectiveContext.searchAlbum, effectiveContext.searchItem];
|
|
54
|
+
return uniqueStrings(parts, 8).join(' ');
|
|
55
|
+
}
|
|
56
|
+
function buildPromptFocus(subtype) {
|
|
57
|
+
switch (subtype) {
|
|
58
|
+
case 'pob':
|
|
59
|
+
return [
|
|
60
|
+
'identify the most likely previous comeback or prior comparable release',
|
|
61
|
+
'look for collector, POB, or preorder demand commentary from the prior cycle',
|
|
62
|
+
'note whether prior demand was strong enough to support current collector-focused monitoring',
|
|
63
|
+
];
|
|
64
|
+
case 'preorder':
|
|
65
|
+
return [
|
|
66
|
+
'identify the most likely previous comeback or prior comparable release',
|
|
67
|
+
'look for prior preorder momentum and collector demand signals',
|
|
68
|
+
'summarize whether earlier release performance supports current preorder confidence',
|
|
69
|
+
];
|
|
70
|
+
case 'event':
|
|
71
|
+
return [
|
|
72
|
+
'identify the closest prior comparable release or event context',
|
|
73
|
+
'summarize prior demand continuity and historical commercial strength',
|
|
74
|
+
'call out ambiguities so live market data can be weighted appropriately',
|
|
75
|
+
];
|
|
76
|
+
case 'album':
|
|
77
|
+
return [
|
|
78
|
+
'identify the immediately previous comeback or album release',
|
|
79
|
+
'find previous first-week sales where support exists',
|
|
80
|
+
'summarize prior release momentum and demand continuity for album tracking',
|
|
81
|
+
];
|
|
82
|
+
default:
|
|
83
|
+
return [
|
|
84
|
+
'identify the best prior comparable release if one exists',
|
|
85
|
+
'summarize commercial strength and demand continuity',
|
|
86
|
+
'flag ambiguity when evidence is weak or mixed',
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function buildPerplexityPrompt(request) {
|
|
91
|
+
const effectiveContext = getValidationEffectiveContext(request);
|
|
92
|
+
const subtype = detectResearchSubtype(request);
|
|
93
|
+
const query = buildResearchQuery(request);
|
|
94
|
+
const promptFocus = buildPromptFocus(subtype);
|
|
95
|
+
const subjectLabel = effectiveContext.sourceType === 'event'
|
|
96
|
+
? (effectiveContext.searchEvent ?? effectiveContext.searchItem ?? 'event context')
|
|
97
|
+
: (effectiveContext.searchAlbum ?? effectiveContext.searchItem ?? request.item.name);
|
|
98
|
+
const userPrompt = [
|
|
99
|
+
'Research the historical commercial context for the following release or validation target.',
|
|
100
|
+
`Primary query: ${query || subjectLabel}`,
|
|
101
|
+
`Artist: ${effectiveContext.searchArtist ?? 'Unknown'}`,
|
|
102
|
+
`Subject: ${subjectLabel}`,
|
|
103
|
+
`Release date: ${effectiveContext.eventDate ?? 'Unknown'}`,
|
|
104
|
+
`Validation type: ${request.validation.validationType}`,
|
|
105
|
+
`Subtype: ${subtype}`,
|
|
106
|
+
`Focus requirements: ${promptFocus.join('; ')}`,
|
|
107
|
+
'Return JSON only with this exact shape:',
|
|
108
|
+
'{',
|
|
109
|
+
' "previousAlbumTitle": string | null,',
|
|
110
|
+
' "previousComebackFirstWeekSales": number | string | null,',
|
|
111
|
+
' "historicalContextNotes": string,',
|
|
112
|
+
' "researchConfidence": "Low" | "Medium" | "High",',
|
|
113
|
+
' "commercialStrengthContext": string | null,',
|
|
114
|
+
' "collectorDemandContext": string | null,',
|
|
115
|
+
' "preorderDemandContext": string | null,',
|
|
116
|
+
' "sourceSnippets": string[],',
|
|
117
|
+
' "ambiguities": string[],',
|
|
118
|
+
' "confidenceReason": string | null,',
|
|
119
|
+
' "scoreReason": string | null,',
|
|
120
|
+
' "notEnoughEvidence": boolean',
|
|
121
|
+
'}',
|
|
122
|
+
'Guidance:',
|
|
123
|
+
'- Prefer factual prior-release context and first-week sales if supported.',
|
|
124
|
+
'- For POB or preorder cases, emphasize prior collector/preorder demand momentum.',
|
|
125
|
+
'- If evidence is uncertain, use nulls, concise ambiguity notes, and lower confidence.',
|
|
126
|
+
'- Keep historicalContextNotes concise and operational for Airtable.',
|
|
127
|
+
].join('\n');
|
|
128
|
+
return { query, promptFocus, userPrompt };
|
|
129
|
+
}
|
|
130
|
+
function extractFirstJsonObject(value) {
|
|
131
|
+
const trimmed = value.trim();
|
|
132
|
+
if (trimmed.length === 0) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const fencedMatch = /```(?:json)?\s*([\s\S]*?)```/i.exec(trimmed);
|
|
136
|
+
const candidate = fencedMatch?.[1]?.trim() ?? trimmed;
|
|
137
|
+
const firstBrace = candidate.indexOf('{');
|
|
138
|
+
if (firstBrace === -1) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
let depth = 0;
|
|
142
|
+
let inString = false;
|
|
143
|
+
let isEscaped = false;
|
|
144
|
+
for (let index = firstBrace; index < candidate.length; index += 1) {
|
|
145
|
+
const character = candidate[index];
|
|
146
|
+
if (inString) {
|
|
147
|
+
if (isEscaped) {
|
|
148
|
+
isEscaped = false;
|
|
149
|
+
}
|
|
150
|
+
else if (character === '\\') {
|
|
151
|
+
isEscaped = true;
|
|
152
|
+
}
|
|
153
|
+
else if (character === '"') {
|
|
154
|
+
inString = false;
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (character === '"') {
|
|
159
|
+
inString = true;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (character === '{') {
|
|
163
|
+
depth += 1;
|
|
164
|
+
}
|
|
165
|
+
else if (character === '}') {
|
|
166
|
+
depth -= 1;
|
|
167
|
+
if (depth === 0) {
|
|
168
|
+
return candidate.slice(firstBrace, index + 1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
function parseResearchResponse(rawContent) {
|
|
175
|
+
const jsonObject = extractFirstJsonObject(rawContent);
|
|
176
|
+
if (jsonObject === null) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
return JSON.parse(jsonObject);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function parseNormalizedSalesValue(amount, unit) {
|
|
187
|
+
const numericPortion = Number(amount);
|
|
188
|
+
if (!Number.isFinite(numericPortion) || numericPortion <= 0) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const normalizedUnit = unit?.toLowerCase() ?? null;
|
|
192
|
+
const multiplier = normalizedUnit === 'm' || normalizedUnit === 'million'
|
|
193
|
+
? 1_000_000
|
|
194
|
+
: normalizedUnit === 'k' || normalizedUnit === 'thousand'
|
|
195
|
+
? 1_000
|
|
196
|
+
: 1;
|
|
197
|
+
return Math.round(numericPortion * multiplier);
|
|
198
|
+
}
|
|
199
|
+
function parseSalesFigure(value) {
|
|
200
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
|
201
|
+
return Math.round(value);
|
|
202
|
+
}
|
|
203
|
+
if (typeof value !== 'string') {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const normalized = value.trim().toLowerCase().replace(/,/g, '');
|
|
207
|
+
if (normalized.length === 0) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
if (/^\d+(?:\.\d+)?$/.test(normalized)) {
|
|
211
|
+
return parseNormalizedSalesValue(normalized);
|
|
212
|
+
}
|
|
213
|
+
const directSalesLike = /^(?:~|about|approximately|approx\.?|around|over|under|nearly|more than|at least)?\s*(\d+(?:\.\d+)?)\s*(m|million|k|thousand)?\s*(copies|albums|units|sales|sold)?\s*$/i.exec(normalized);
|
|
214
|
+
if (directSalesLike !== null && (directSalesLike[2] || directSalesLike[3])) {
|
|
215
|
+
return parseNormalizedSalesValue(directSalesLike[1], directSalesLike[2]);
|
|
216
|
+
}
|
|
217
|
+
const numericTokens = Array.from(normalized.matchAll(/\d+(?:\.\d+)?/g)).length;
|
|
218
|
+
if (numericTokens !== 1) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const contextualSalesLike = /(?:first[-\s]?week(?:\s+\w+){0,3}\s+sales|first[-\s]?week|sales|sold|copies|albums|units)[^\d]{0,24}(\d+(?:\.\d+)?)\s*(m|million|k|thousand)?\b/i.exec(normalized);
|
|
222
|
+
if (contextualSalesLike === null) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
return parseNormalizedSalesValue(contextualSalesLike[1], contextualSalesLike[2]);
|
|
226
|
+
}
|
|
227
|
+
function normalizeConfidence(value, hasSubstantiveEvidence) {
|
|
228
|
+
if (!hasSubstantiveEvidence) {
|
|
229
|
+
return 'Low';
|
|
230
|
+
}
|
|
231
|
+
if (typeof value === 'string') {
|
|
232
|
+
const normalized = value.trim().toLowerCase();
|
|
233
|
+
if (normalized === 'high') {
|
|
234
|
+
return 'High';
|
|
235
|
+
}
|
|
236
|
+
if (normalized === 'medium') {
|
|
237
|
+
return 'Medium';
|
|
238
|
+
}
|
|
239
|
+
if (normalized === 'low') {
|
|
240
|
+
return 'Low';
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return 'Medium';
|
|
244
|
+
}
|
|
245
|
+
function buildHistoricalNotes(parsed, subtype) {
|
|
246
|
+
const explicitNotes = sanitizeText(parsed.historicalContextNotes);
|
|
247
|
+
if (explicitNotes !== null) {
|
|
248
|
+
return truncateText(explicitNotes, 320);
|
|
249
|
+
}
|
|
250
|
+
const parts = [
|
|
251
|
+
sanitizeText(parsed.previousAlbumTitle)
|
|
252
|
+
? `Previous release identified as ${sanitizeText(parsed.previousAlbumTitle)}.`
|
|
253
|
+
: null,
|
|
254
|
+
parseSalesFigure(parsed.previousComebackFirstWeekSales)
|
|
255
|
+
? `Reported first-week sales were approximately ${parseSalesFigure(parsed.previousComebackFirstWeekSales)}.`
|
|
256
|
+
: null,
|
|
257
|
+
sanitizeText(parsed.commercialStrengthContext),
|
|
258
|
+
subtype === 'pob' || subtype === 'preorder'
|
|
259
|
+
? (sanitizeText(parsed.preorderDemandContext) ?? sanitizeText(parsed.collectorDemandContext))
|
|
260
|
+
: sanitizeText(parsed.collectorDemandContext),
|
|
261
|
+
sanitizeText(parsed.ambiguities?.[0]),
|
|
262
|
+
];
|
|
263
|
+
const joined = uniqueStrings(parts, 5).join(' ');
|
|
264
|
+
return joined.length > 0
|
|
265
|
+
? truncateText(joined, 320)
|
|
266
|
+
: 'Limited prior-market evidence found. Context weak; rely more on live market signals.';
|
|
267
|
+
}
|
|
268
|
+
function computeHistoricalContextScore(input) {
|
|
269
|
+
const hasSubstantiveEvidence = input.previousAlbumTitle !== null ||
|
|
270
|
+
input.previousComebackFirstWeekSales !== null ||
|
|
271
|
+
input.demandContextPresent;
|
|
272
|
+
if (!hasSubstantiveEvidence) {
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
if (input.notEnoughEvidence &&
|
|
276
|
+
input.previousAlbumTitle === null &&
|
|
277
|
+
input.previousComebackFirstWeekSales === null &&
|
|
278
|
+
!input.demandContextPresent) {
|
|
279
|
+
return 0;
|
|
280
|
+
}
|
|
281
|
+
let score = 0;
|
|
282
|
+
if (input.previousAlbumTitle !== null) {
|
|
283
|
+
score += 6;
|
|
284
|
+
}
|
|
285
|
+
if (input.previousComebackFirstWeekSales !== null) {
|
|
286
|
+
score += 6;
|
|
287
|
+
}
|
|
288
|
+
if (input.demandContextPresent) {
|
|
289
|
+
score += 4;
|
|
290
|
+
}
|
|
291
|
+
if (input.snippetCount >= 2) {
|
|
292
|
+
score += 2;
|
|
293
|
+
}
|
|
294
|
+
if (input.snippetCount >= 3) {
|
|
295
|
+
score += 2;
|
|
296
|
+
}
|
|
297
|
+
if (score === 0) {
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
if (input.confidence === 'High') {
|
|
301
|
+
return clamp(Math.max(score, 15), 15, 20);
|
|
302
|
+
}
|
|
303
|
+
if (input.confidence === 'Medium') {
|
|
304
|
+
return clamp(Math.max(score, 8), 8, 14);
|
|
305
|
+
}
|
|
306
|
+
return clamp(Math.max(score, 1), 1, 7);
|
|
307
|
+
}
|
|
308
|
+
function buildScoreAssignmentReason(input) {
|
|
309
|
+
const factors = [
|
|
310
|
+
input.previousAlbumTitle !== null ? 'resolved prior release' : null,
|
|
311
|
+
input.previousComebackFirstWeekSales !== null ? 'supported first-week sales' : null,
|
|
312
|
+
input.demandContextPresent ? 'collector/preorder demand context' : null,
|
|
313
|
+
input.snippetCount > 0
|
|
314
|
+
? `${input.snippetCount} supporting snippet${input.snippetCount === 1 ? '' : 's'}`
|
|
315
|
+
: null,
|
|
316
|
+
].filter((value) => value !== null);
|
|
317
|
+
const defaultReason = factors.length > 0
|
|
318
|
+
? `${input.confidence} confidence based on ${factors.join(', ')}; assigned normalized historical score ${input.score}.`
|
|
319
|
+
: `No reliable historical evidence was normalized, so the historical score remained ${input.score}.`;
|
|
320
|
+
const parsedReason = sanitizeText(input.parsedScoreReason);
|
|
321
|
+
return parsedReason !== null ? truncateText(parsedReason, 240) : defaultReason;
|
|
322
|
+
}
|
|
323
|
+
function buildFallbackResearchSignals(input) {
|
|
324
|
+
const confidence = input.confidence ?? 'Low';
|
|
325
|
+
const historicalContextNotes = truncateText(input.notes, 320);
|
|
7
326
|
return {
|
|
8
327
|
previousAlbumTitle: null,
|
|
9
328
|
previousComebackFirstWeekSales: null,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
329
|
+
perplexityHistoricalContextScore: 0,
|
|
330
|
+
historicalContextNotes,
|
|
331
|
+
confidence,
|
|
332
|
+
notes: historicalContextNotes,
|
|
14
333
|
sources: [],
|
|
334
|
+
debug: {
|
|
335
|
+
providerStatus: input.providerStatus,
|
|
336
|
+
parseStatus: input.parseStatus,
|
|
337
|
+
query: input.query ?? null,
|
|
338
|
+
promptFocus: input.promptFocus ?? [],
|
|
339
|
+
citations: [],
|
|
340
|
+
sourceSnippets: [],
|
|
341
|
+
resolvedPriorRelease: null,
|
|
342
|
+
extractedConfidence: null,
|
|
343
|
+
computedConfidence: confidence,
|
|
344
|
+
confidenceReason: null,
|
|
345
|
+
scoreAssignmentReason: input.providerStatus === 'error'
|
|
346
|
+
? 'Historical research request failed, so the provider returned a zero historical score.'
|
|
347
|
+
: 'Historical research did not yield structured evidence, so the provider returned a zero historical score.',
|
|
348
|
+
rawResponseText: input.rawResponseText ?? null,
|
|
349
|
+
errorMessage: input.errorMessage ?? null,
|
|
350
|
+
},
|
|
15
351
|
};
|
|
16
352
|
}
|
|
353
|
+
export async function getPreviousComebackResearchSignals(request) {
|
|
354
|
+
const hasPerplexityKey = (process.env.PERPLEXITY_API_KEY ?? '').trim().length > 0;
|
|
355
|
+
const effectiveContext = getValidationEffectiveContext(request);
|
|
356
|
+
const primaryAlbum = effectiveContext.searchAlbum ?? effectiveContext.searchEvent ?? effectiveContext.searchItem;
|
|
357
|
+
const { query, promptFocus, userPrompt } = buildPerplexityPrompt(request);
|
|
358
|
+
if (!hasPerplexityKey) {
|
|
359
|
+
return buildFallbackResearchSignals({
|
|
360
|
+
notes: `PERPLEXITY_API_KEY is not configured. Historical context for ${primaryAlbum ?? 'this release'} remains unverified; rely more on live market signals.`,
|
|
361
|
+
providerStatus: 'unconfigured',
|
|
362
|
+
parseStatus: 'unconfigured',
|
|
363
|
+
query,
|
|
364
|
+
promptFocus,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (query.length === 0) {
|
|
368
|
+
return buildFallbackResearchSignals({
|
|
369
|
+
notes: 'Historical research query could not be derived from the current validation context.',
|
|
370
|
+
providerStatus: 'no_evidence',
|
|
371
|
+
parseStatus: 'fallback',
|
|
372
|
+
query: null,
|
|
373
|
+
promptFocus,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
const controller = new AbortController();
|
|
377
|
+
const timeout = setTimeout(() => controller.abort(), 30_000);
|
|
378
|
+
try {
|
|
379
|
+
const response = await fetch('https://api.perplexity.ai/chat/completions', {
|
|
380
|
+
method: 'POST',
|
|
381
|
+
headers: {
|
|
382
|
+
'Content-Type': 'application/json',
|
|
383
|
+
Authorization: `Bearer ${process.env.PERPLEXITY_API_KEY ?? ''}`,
|
|
384
|
+
},
|
|
385
|
+
body: JSON.stringify({
|
|
386
|
+
model: 'sonar',
|
|
387
|
+
temperature: 0.1,
|
|
388
|
+
max_tokens: 900,
|
|
389
|
+
messages: [
|
|
390
|
+
{
|
|
391
|
+
role: 'system',
|
|
392
|
+
content: 'You are a historical music market research assistant. Use grounded web research, stay cautious with ambiguous evidence, and return JSON only.',
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
role: 'user',
|
|
396
|
+
content: userPrompt,
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
}),
|
|
400
|
+
signal: controller.signal,
|
|
401
|
+
});
|
|
402
|
+
const responseText = await response.text();
|
|
403
|
+
if (!response.ok) {
|
|
404
|
+
return buildFallbackResearchSignals({
|
|
405
|
+
notes: `Perplexity historical research request failed for ${primaryAlbum ?? 'this release'}.`,
|
|
406
|
+
providerStatus: 'error',
|
|
407
|
+
parseStatus: 'error',
|
|
408
|
+
query,
|
|
409
|
+
promptFocus,
|
|
410
|
+
rawResponseText: responseText,
|
|
411
|
+
errorMessage: `HTTP ${response.status}`,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
const parsedResponse = JSON.parse(responseText);
|
|
415
|
+
const rawContent = parsedResponse.choices?.[0]?.message?.content?.trim() ?? '';
|
|
416
|
+
const parsed = parseResearchResponse(rawContent);
|
|
417
|
+
if (parsed === null) {
|
|
418
|
+
return buildFallbackResearchSignals({
|
|
419
|
+
notes: `Perplexity returned an unstructured historical response for ${primaryAlbum ?? 'this release'}, so live market signals should carry more weight.`,
|
|
420
|
+
providerStatus: 'error',
|
|
421
|
+
parseStatus: 'error',
|
|
422
|
+
query,
|
|
423
|
+
promptFocus,
|
|
424
|
+
rawResponseText: rawContent,
|
|
425
|
+
errorMessage: 'Unable to normalize Perplexity response into structured JSON.',
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
const previousAlbumTitle = sanitizeText(parsed.previousAlbumTitle);
|
|
429
|
+
const previousComebackFirstWeekSales = parseSalesFigure(parsed.previousComebackFirstWeekSales);
|
|
430
|
+
const demandContextPresent = sanitizeText(parsed.commercialStrengthContext) !== null ||
|
|
431
|
+
sanitizeText(parsed.collectorDemandContext) !== null ||
|
|
432
|
+
sanitizeText(parsed.preorderDemandContext) !== null;
|
|
433
|
+
const sourceSnippets = uniqueStrings(parsed.sourceSnippets ?? [], 4);
|
|
434
|
+
const citations = uniqueStrings(parsedResponse.citations ?? [], 6);
|
|
435
|
+
const hasSubstantiveEvidence = previousAlbumTitle !== null ||
|
|
436
|
+
previousComebackFirstWeekSales !== null ||
|
|
437
|
+
demandContextPresent;
|
|
438
|
+
const confidence = normalizeConfidence(parsed.researchConfidence, hasSubstantiveEvidence);
|
|
439
|
+
const score = computeHistoricalContextScore({
|
|
440
|
+
confidence,
|
|
441
|
+
previousAlbumTitle,
|
|
442
|
+
previousComebackFirstWeekSales,
|
|
443
|
+
demandContextPresent,
|
|
444
|
+
snippetCount: sourceSnippets.length + Math.min(citations.length, 2),
|
|
445
|
+
notEnoughEvidence: parsed.notEnoughEvidence === true,
|
|
446
|
+
});
|
|
447
|
+
const historicalContextNotes = buildHistoricalNotes(parsed, detectResearchSubtype(request));
|
|
448
|
+
const scoreAssignmentReason = buildScoreAssignmentReason({
|
|
449
|
+
confidence,
|
|
450
|
+
previousAlbumTitle,
|
|
451
|
+
previousComebackFirstWeekSales,
|
|
452
|
+
demandContextPresent,
|
|
453
|
+
snippetCount: sourceSnippets.length + citations.length,
|
|
454
|
+
score,
|
|
455
|
+
parsedScoreReason: sanitizeText(parsed.scoreReason),
|
|
456
|
+
});
|
|
457
|
+
return {
|
|
458
|
+
previousAlbumTitle,
|
|
459
|
+
previousComebackFirstWeekSales,
|
|
460
|
+
perplexityHistoricalContextScore: score,
|
|
461
|
+
historicalContextNotes,
|
|
462
|
+
confidence,
|
|
463
|
+
notes: historicalContextNotes,
|
|
464
|
+
sources: citations,
|
|
465
|
+
debug: {
|
|
466
|
+
providerStatus: hasSubstantiveEvidence ? 'ok' : 'no_evidence',
|
|
467
|
+
parseStatus: 'ok',
|
|
468
|
+
query,
|
|
469
|
+
promptFocus,
|
|
470
|
+
citations,
|
|
471
|
+
sourceSnippets,
|
|
472
|
+
resolvedPriorRelease: previousAlbumTitle,
|
|
473
|
+
extractedConfidence: sanitizeText(parsed.researchConfidence)
|
|
474
|
+
? normalizeConfidence(parsed.researchConfidence, hasSubstantiveEvidence)
|
|
475
|
+
: null,
|
|
476
|
+
computedConfidence: confidence,
|
|
477
|
+
confidenceReason: sanitizeText(parsed.confidenceReason),
|
|
478
|
+
scoreAssignmentReason,
|
|
479
|
+
rawResponseText: rawContent,
|
|
480
|
+
errorMessage: null,
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
return buildFallbackResearchSignals({
|
|
486
|
+
notes: `Historical research lookup failed for ${primaryAlbum ?? 'this release'}. Context remains weak until live market data improves.`,
|
|
487
|
+
providerStatus: 'error',
|
|
488
|
+
parseStatus: 'error',
|
|
489
|
+
query,
|
|
490
|
+
promptFocus,
|
|
491
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
finally {
|
|
495
|
+
clearTimeout(timeout);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
@@ -35,8 +35,15 @@ function hasUsableTrackingQuery(effectiveContext) {
|
|
|
35
35
|
}
|
|
36
36
|
return buildFallbackTrackingQuery(effectiveContext) !== null;
|
|
37
37
|
}
|
|
38
|
-
function resolvePreferredSoldValue(
|
|
39
|
-
return
|
|
38
|
+
function resolvePreferredSoldValue(researchValue, soldValue) {
|
|
39
|
+
return researchValue ?? soldValue;
|
|
40
|
+
}
|
|
41
|
+
function hasResearchSoldEvidence(signals) {
|
|
42
|
+
return (signals.terapeak.provider === 'ebay_research_ui' &&
|
|
43
|
+
(signals.terapeak.researchSoldPriceUsd !== null ||
|
|
44
|
+
signals.terapeak.soldListingsCount !== null ||
|
|
45
|
+
signals.terapeak.recentSoldCount7d !== null ||
|
|
46
|
+
Object.values(signals.terapeak.soldVelocity).some((value) => value !== null)));
|
|
40
47
|
}
|
|
41
48
|
export function buildValidationRecommendation(request, signals) {
|
|
42
49
|
const dDay = request.validation.dDay;
|
|
@@ -73,9 +80,9 @@ export function buildValidationRecommendation(request, signals) {
|
|
|
73
80
|
request.item.name ??
|
|
74
81
|
'release');
|
|
75
82
|
const mergedSoldVelocity = {
|
|
76
|
-
day1Sold: resolvePreferredSoldValue(signals.
|
|
77
|
-
day2Sold: resolvePreferredSoldValue(signals.
|
|
78
|
-
day3Sold: resolvePreferredSoldValue(signals.
|
|
83
|
+
day1Sold: resolvePreferredSoldValue(signals.terapeak.soldVelocity.day1Sold, signals.sold.soldVelocity.day1Sold),
|
|
84
|
+
day2Sold: resolvePreferredSoldValue(signals.terapeak.soldVelocity.day2Sold, signals.sold.soldVelocity.day2Sold),
|
|
85
|
+
day3Sold: resolvePreferredSoldValue(signals.terapeak.soldVelocity.day3Sold, signals.sold.soldVelocity.day3Sold),
|
|
79
86
|
};
|
|
80
87
|
const recentSoldCount = [
|
|
81
88
|
mergedSoldVelocity.day1Sold,
|
|
@@ -85,17 +92,19 @@ export function buildValidationRecommendation(request, signals) {
|
|
|
85
92
|
const hasSoldProviderEvidence = signals.sold.soldMedianPriceUsd !== null ||
|
|
86
93
|
signals.sold.soldResultsCount !== null ||
|
|
87
94
|
Object.values(signals.sold.soldVelocity).some((value) => value !== null);
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
? signals.sold.confidence
|
|
96
|
-
: hasResearchSoldEvidence
|
|
97
|
-
? signals.terapeak.confidence
|
|
95
|
+
const researchSoldEvidence = hasResearchSoldEvidence(signals);
|
|
96
|
+
const effectiveSoldComparablePrice = (researchSoldEvidence ? signals.terapeak.researchSoldPriceUsd : null) ??
|
|
97
|
+
signals.sold.soldMedianPriceUsd;
|
|
98
|
+
const effectiveSoldConfidence = researchSoldEvidence
|
|
99
|
+
? signals.terapeak.confidence
|
|
100
|
+
: hasSoldProviderEvidence
|
|
101
|
+
? signals.sold.confidence
|
|
98
102
|
: 'Low';
|
|
103
|
+
const hasUsableHistoricalResearch = signals.research.debug?.providerStatus !== undefined
|
|
104
|
+
? signals.research.debug.providerStatus === 'ok'
|
|
105
|
+
: signals.research.previousAlbumTitle !== null ||
|
|
106
|
+
signals.research.previousComebackFirstWeekSales !== null ||
|
|
107
|
+
signals.research.perplexityHistoricalContextScore > 0;
|
|
99
108
|
let latestAiRecommendation = 'Continue watching until stronger market signal appears.';
|
|
100
109
|
let latestAiConfidence = 'Medium';
|
|
101
110
|
let monitoringNotes = `Baseline recommendation generated from current ${signals.effectiveContext.mode} validation state for ${subjectLabel}.`;
|
|
@@ -157,9 +166,12 @@ export function buildValidationRecommendation(request, signals) {
|
|
|
157
166
|
latestAiConfidence = 'Medium';
|
|
158
167
|
}
|
|
159
168
|
}
|
|
160
|
-
if (signals.research.previousComebackFirstWeekSales !== null) {
|
|
169
|
+
if (hasUsableHistoricalResearch && signals.research.previousComebackFirstWeekSales !== null) {
|
|
161
170
|
monitoringNotes += ` Previous comeback first-week sales reference: ${signals.research.previousComebackFirstWeekSales}.`;
|
|
162
171
|
}
|
|
172
|
+
if (hasUsableHistoricalResearch && signals.research.historicalContextNotes.length > 0) {
|
|
173
|
+
monitoringNotes += ` Historical context (${signals.research.confidence}, score ${signals.research.perplexityHistoricalContextScore}/20): ${signals.research.historicalContextNotes}`;
|
|
174
|
+
}
|
|
163
175
|
return {
|
|
164
176
|
buyDecision: request.validation.buyDecision,
|
|
165
177
|
automationStatus: shouldAutoTrack ? 'Watching' : 'Paused',
|
|
@@ -49,13 +49,69 @@ function getFieldPresence(fields) {
|
|
|
49
49
|
function getWriteSource(value, source) {
|
|
50
50
|
return isMeaningfulWriteValue(value) ? source : 'none';
|
|
51
51
|
}
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
function hasSoldVelocityEvidence(soldVelocity) {
|
|
53
|
+
return Object.values(soldVelocity).some((value) => value !== null);
|
|
54
|
+
}
|
|
55
|
+
function hasPrimaryResearchSoldSignals(terapeak) {
|
|
56
|
+
return (terapeak.provider === 'ebay_research_ui' &&
|
|
57
|
+
(terapeak.researchSoldPriceUsd !== null ||
|
|
58
|
+
terapeak.soldListingsCount !== null ||
|
|
59
|
+
terapeak.recentSoldCount7d !== null ||
|
|
60
|
+
hasSoldVelocityEvidence(terapeak.soldVelocity)));
|
|
61
|
+
}
|
|
62
|
+
function createSkippedSoldSignals() {
|
|
63
|
+
return {
|
|
64
|
+
provider: 'third_party_sold_api',
|
|
65
|
+
confidence: 'Low',
|
|
66
|
+
soldResultsCount: null,
|
|
67
|
+
soldAveragePriceUsd: null,
|
|
68
|
+
soldMedianPriceUsd: null,
|
|
69
|
+
soldMinPriceUsd: null,
|
|
70
|
+
soldMaxPriceUsd: null,
|
|
71
|
+
soldItemsSample: [],
|
|
72
|
+
soldVelocity: {
|
|
73
|
+
day1Sold: null,
|
|
74
|
+
day2Sold: null,
|
|
75
|
+
day3Sold: null,
|
|
76
|
+
day4Sold: null,
|
|
77
|
+
day5Sold: null,
|
|
78
|
+
daysTracked: null,
|
|
79
|
+
},
|
|
80
|
+
recentSoldCount7d: null,
|
|
81
|
+
soldBucketDebug: {
|
|
82
|
+
status: 'skipped',
|
|
83
|
+
notes: [
|
|
84
|
+
'legacy sold fallback was skipped because first-party research sold signals were sufficient',
|
|
85
|
+
],
|
|
86
|
+
totalItemsExamined: 0,
|
|
87
|
+
withSoldAt: 0,
|
|
88
|
+
missingSoldAt: 0,
|
|
89
|
+
dateParseFailures: 0,
|
|
90
|
+
futureDated: 0,
|
|
91
|
+
bucketedItems: 0,
|
|
92
|
+
},
|
|
93
|
+
query: null,
|
|
94
|
+
queryCandidates: [],
|
|
95
|
+
queryDiagnostics: [],
|
|
96
|
+
selectedQuery: undefined,
|
|
97
|
+
selectedQueryTier: null,
|
|
98
|
+
selectedQueryFamily: null,
|
|
99
|
+
broadAlbumQuery: null,
|
|
100
|
+
subtypeSpecificQuery: null,
|
|
101
|
+
querySelectionReason: 'Legacy sold fallback was skipped because first-party research sold signals were sufficient.',
|
|
102
|
+
responseUrl: null,
|
|
103
|
+
status: 'skipped',
|
|
104
|
+
errorMessage: undefined,
|
|
105
|
+
queryResolution: undefined,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function resolvePreferredSoldMetric(terapeakValue, soldValue, ebayValue) {
|
|
56
109
|
if (terapeakValue !== null) {
|
|
57
110
|
return { value: terapeakValue, source: 'terapeak' };
|
|
58
111
|
}
|
|
112
|
+
if (soldValue !== null) {
|
|
113
|
+
return { value: soldValue, source: 'sold' };
|
|
114
|
+
}
|
|
59
115
|
if (ebayValue !== null) {
|
|
60
116
|
return { value: ebayValue, source: 'ebay' };
|
|
61
117
|
}
|
|
@@ -105,6 +161,9 @@ function buildProviderDebug(request, ebay, sold, terapeak, social, chart, resear
|
|
|
105
161
|
const researchFields = getFieldPresence({
|
|
106
162
|
previousAlbumTitle: research.previousAlbumTitle,
|
|
107
163
|
previousComebackFirstWeekSales: research.previousComebackFirstWeekSales,
|
|
164
|
+
perplexityHistoricalContextScore: research.perplexityHistoricalContextScore,
|
|
165
|
+
historicalContextNotes: research.historicalContextNotes,
|
|
166
|
+
researchConfidence: research.confidence,
|
|
108
167
|
});
|
|
109
168
|
const ebayStatus = (ebay.queryCandidates?.length ?? 0) === 0
|
|
110
169
|
? 'unavailable'
|
|
@@ -117,9 +176,15 @@ function buildProviderDebug(request, ebay, sold, terapeak, social, chart, resear
|
|
|
117
176
|
: terapeakFields.contributed.length > 0
|
|
118
177
|
? 'ok'
|
|
119
178
|
: 'partial';
|
|
120
|
-
const researchStatus = research.
|
|
121
|
-
? '
|
|
122
|
-
: '
|
|
179
|
+
const researchStatus = research.debug?.providerStatus === 'unconfigured'
|
|
180
|
+
? 'unavailable'
|
|
181
|
+
: research.debug?.providerStatus === 'error'
|
|
182
|
+
? 'partial'
|
|
183
|
+
: research.previousComebackFirstWeekSales !== null ||
|
|
184
|
+
research.previousAlbumTitle !== null ||
|
|
185
|
+
research.perplexityHistoricalContextScore > 0
|
|
186
|
+
? 'ok'
|
|
187
|
+
: 'partial';
|
|
123
188
|
return {
|
|
124
189
|
ebay: {
|
|
125
190
|
status: ebayStatus,
|
|
@@ -225,10 +290,23 @@ function buildProviderDebug(request, ebay, sold, terapeak, social, chart, resear
|
|
|
225
290
|
confidence: research.confidence.toLowerCase(),
|
|
226
291
|
previousAlbumTitle: research.previousAlbumTitle,
|
|
227
292
|
previousComebackFirstWeekSales: research.previousComebackFirstWeekSales,
|
|
293
|
+
perplexityHistoricalContextScore: research.perplexityHistoricalContextScore,
|
|
294
|
+
historicalContextNotes: research.historicalContextNotes,
|
|
228
295
|
contributedFields: researchFields.contributed,
|
|
229
296
|
omittedFields: researchFields.omitted,
|
|
230
297
|
notes: research.notes,
|
|
231
298
|
sources: research.sources ?? [],
|
|
299
|
+
query: research.debug?.query ?? null,
|
|
300
|
+
promptFocus: research.debug?.promptFocus ?? [],
|
|
301
|
+
sourceSnippets: research.debug?.sourceSnippets ?? [],
|
|
302
|
+
resolvedPriorRelease: research.debug?.resolvedPriorRelease ?? research.previousAlbumTitle,
|
|
303
|
+
extractedConfidence: research.debug?.extractedConfidence ?? null,
|
|
304
|
+
computedConfidence: research.debug?.computedConfidence ?? research.confidence,
|
|
305
|
+
confidenceReason: research.debug?.confidenceReason ?? null,
|
|
306
|
+
scoreAssignmentReason: research.debug?.scoreAssignmentReason ?? null,
|
|
307
|
+
providerStatus: research.debug?.providerStatus ?? null,
|
|
308
|
+
parseStatus: research.debug?.parseStatus ?? null,
|
|
309
|
+
errorMessage: research.debug?.errorMessage ?? null,
|
|
232
310
|
},
|
|
233
311
|
};
|
|
234
312
|
}
|
|
@@ -254,8 +332,11 @@ export async function runValidation(api, input) {
|
|
|
254
332
|
effectiveContext,
|
|
255
333
|
};
|
|
256
334
|
const ebay = await getEbayValidationSignals(api, effectiveRequest);
|
|
257
|
-
const sold = await getEbaySoldValidationSignals(effectiveRequest);
|
|
258
335
|
const terapeak = await getTerapeakValidationSignals(api, effectiveRequest);
|
|
336
|
+
const primaryResearchSoldSignalsAvailable = hasPrimaryResearchSoldSignals(terapeak);
|
|
337
|
+
const sold = primaryResearchSoldSignalsAvailable
|
|
338
|
+
? createSkippedSoldSignals()
|
|
339
|
+
: await getEbaySoldValidationSignals(effectiveRequest);
|
|
259
340
|
const social = await getSocialValidationSignals(effectiveRequest);
|
|
260
341
|
const chart = getChartValidationSignals(effectiveRequest);
|
|
261
342
|
const research = await getPreviousComebackResearchSignals(effectiveRequest);
|
|
@@ -264,12 +345,12 @@ export async function runValidation(api, input) {
|
|
|
264
345
|
const marketPriceUsd = terapeak.marketPriceUsd ?? sold.soldMedianPriceUsd ?? ebay.marketPriceUsd;
|
|
265
346
|
const mergedAvgShippingCostUsd = terapeak.avgShippingCostUsd ?? ebay.avgShippingCostUsd;
|
|
266
347
|
const mergedCompetitionLevel = terapeak.competitionLevel ?? ebay.competitionLevel;
|
|
267
|
-
const day1Sold = resolvePreferredSoldMetric(
|
|
268
|
-
const day2Sold = resolvePreferredSoldMetric(
|
|
269
|
-
const day3Sold = resolvePreferredSoldMetric(
|
|
270
|
-
const day4Sold = resolvePreferredSoldMetric(
|
|
271
|
-
const day5Sold = resolvePreferredSoldMetric(
|
|
272
|
-
const daysTracked = resolvePreferredSoldMetric(
|
|
348
|
+
const day1Sold = resolvePreferredSoldMetric(terapeak.soldVelocity.day1Sold, sold.soldVelocity.day1Sold, ebay.soldVelocity.day1Sold);
|
|
349
|
+
const day2Sold = resolvePreferredSoldMetric(terapeak.soldVelocity.day2Sold, sold.soldVelocity.day2Sold, ebay.soldVelocity.day2Sold);
|
|
350
|
+
const day3Sold = resolvePreferredSoldMetric(terapeak.soldVelocity.day3Sold, sold.soldVelocity.day3Sold, ebay.soldVelocity.day3Sold);
|
|
351
|
+
const day4Sold = resolvePreferredSoldMetric(terapeak.soldVelocity.day4Sold, sold.soldVelocity.day4Sold, ebay.soldVelocity.day4Sold);
|
|
352
|
+
const day5Sold = resolvePreferredSoldMetric(terapeak.soldVelocity.day5Sold, sold.soldVelocity.day5Sold, ebay.soldVelocity.day5Sold);
|
|
353
|
+
const daysTracked = resolvePreferredSoldMetric(terapeak.soldVelocity.daysTracked, sold.soldVelocity.daysTracked, ebay.soldVelocity.daysTracked);
|
|
273
354
|
const soldVelocity = {
|
|
274
355
|
day1Sold: day1Sold.value,
|
|
275
356
|
day2Sold: day2Sold.value,
|
|
@@ -289,6 +370,39 @@ export async function runValidation(api, input) {
|
|
|
289
370
|
});
|
|
290
371
|
const requestQueryResolution = buildProviderQueryResolutionDebug(effectiveRequest, Boolean(ebay.queryResolution?.queryContextUsed));
|
|
291
372
|
const mergedSignals = { effectiveContext, ebay, sold, terapeak, social, chart, research };
|
|
373
|
+
const activeSource = terapeak.avgWatchersPerListing !== null ||
|
|
374
|
+
terapeak.preOrderListingsCount !== null ||
|
|
375
|
+
terapeak.competitionLevel !== null
|
|
376
|
+
? 'ebay_research_ui'
|
|
377
|
+
: ebay.preOrderListingsCount !== null ||
|
|
378
|
+
ebay.marketPriceUsd !== null ||
|
|
379
|
+
ebay.competitionLevel !== null
|
|
380
|
+
? 'ebay_browse'
|
|
381
|
+
: 'none';
|
|
382
|
+
const soldSource = primaryResearchSoldSignalsAvailable
|
|
383
|
+
? 'ebay_research_ui'
|
|
384
|
+
: sold.soldMedianPriceUsd !== null ||
|
|
385
|
+
sold.soldResultsCount !== null ||
|
|
386
|
+
hasSoldVelocityEvidence(sold.soldVelocity)
|
|
387
|
+
? 'third_party_sold_api'
|
|
388
|
+
: ebay.marketPriceUsd !== null
|
|
389
|
+
? 'ebay_browse'
|
|
390
|
+
: 'none';
|
|
391
|
+
const providerResolution = {
|
|
392
|
+
activeSource,
|
|
393
|
+
soldSource,
|
|
394
|
+
soldFallbackUsed: !primaryResearchSoldSignalsAvailable && soldSource === 'third_party_sold_api',
|
|
395
|
+
fallbackReason: primaryResearchSoldSignalsAvailable
|
|
396
|
+
? null
|
|
397
|
+
: soldSource === 'third_party_sold_api'
|
|
398
|
+
? 'First-party research sold signals were unavailable or insufficient, so the legacy sold provider was used as automatic fallback.'
|
|
399
|
+
: 'First-party research sold signals were unavailable or insufficient, but no legacy sold fallback data was available.',
|
|
400
|
+
};
|
|
401
|
+
const hasUsableHistoricalResearch = research.debug?.providerStatus !== undefined
|
|
402
|
+
? research.debug.providerStatus === 'ok'
|
|
403
|
+
: research.previousAlbumTitle !== null ||
|
|
404
|
+
research.previousComebackFirstWeekSales !== null ||
|
|
405
|
+
research.perplexityHistoricalContextScore > 0;
|
|
292
406
|
const socialWrites = {
|
|
293
407
|
...(social.twitterTrending !== null ? { twitterTrending: social.twitterTrending } : {}),
|
|
294
408
|
...(social.youtubeViews24hMillions !== null
|
|
@@ -307,7 +421,14 @@ export async function runValidation(api, input) {
|
|
|
307
421
|
: {}),
|
|
308
422
|
};
|
|
309
423
|
const researchWrites = {
|
|
310
|
-
...(
|
|
424
|
+
...(hasUsableHistoricalResearch
|
|
425
|
+
? {
|
|
426
|
+
perplexityHistoricalContextScore: research.perplexityHistoricalContextScore,
|
|
427
|
+
historicalContextNotes: research.historicalContextNotes,
|
|
428
|
+
researchConfidence: research.confidence,
|
|
429
|
+
}
|
|
430
|
+
: {}),
|
|
431
|
+
...(hasUsableHistoricalResearch && research.previousComebackFirstWeekSales !== null
|
|
311
432
|
? { previousComebackFirstWeekSales: research.previousComebackFirstWeekSales }
|
|
312
433
|
: {}),
|
|
313
434
|
};
|
|
@@ -356,7 +477,10 @@ export async function runValidation(api, input) {
|
|
|
356
477
|
previousPobSellThroughPct: terapeak.previousPobSellThroughPct !== null
|
|
357
478
|
? (terapeak.queryDebug.writeSources?.previousPobSellThroughPct ?? 'terapeak')
|
|
358
479
|
: 'none',
|
|
359
|
-
previousComebackFirstWeekSales: getWriteSource(research.previousComebackFirstWeekSales, 'research'),
|
|
480
|
+
previousComebackFirstWeekSales: getWriteSource(hasUsableHistoricalResearch ? research.previousComebackFirstWeekSales : null, 'research'),
|
|
481
|
+
perplexityHistoricalContextScore: hasUsableHistoricalResearch ? 'research' : 'none',
|
|
482
|
+
historicalContextNotes: hasUsableHistoricalResearch ? 'research' : 'none',
|
|
483
|
+
researchConfidence: hasUsableHistoricalResearch ? 'research' : 'none',
|
|
360
484
|
};
|
|
361
485
|
const omittedOptionalWrites = Object.entries(writeResolution)
|
|
362
486
|
.filter(([, source]) => source === 'none')
|
|
@@ -412,6 +536,7 @@ export async function runValidation(api, input) {
|
|
|
412
536
|
soldResultsCount: sold.soldResultsCount,
|
|
413
537
|
omittedOptionalWrites,
|
|
414
538
|
writeResolution,
|
|
539
|
+
providerResolution,
|
|
415
540
|
sourceSet: ['ebay', 'sold', 'terapeak', 'social', 'chart', 'research'],
|
|
416
541
|
providers: buildProviderDebug(effectiveRequest, ebay, sold, terapeak, social, chart, research),
|
|
417
542
|
queryContextUsed: requestQueryResolution.queryContextUsed,
|
|
@@ -111,6 +111,9 @@ export const validationWritesSchema = z.object({
|
|
|
111
111
|
previousPobAvgPriceUsd: z.number().nullable().optional(),
|
|
112
112
|
previousPobSellThroughPct: z.number().nullable().optional(),
|
|
113
113
|
previousComebackFirstWeekSales: z.number().nullable().optional(),
|
|
114
|
+
perplexityHistoricalContextScore: z.number().optional(),
|
|
115
|
+
historicalContextNotes: z.string().optional(),
|
|
116
|
+
researchConfidence: z.enum(['High', 'Medium', 'Low']).optional(),
|
|
114
117
|
monitoringNotes: z.string().optional(),
|
|
115
118
|
lastDataSnapshot: z.string().optional(),
|
|
116
119
|
latestAiRecommendation: z.string().optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ebay-mcp-remote-edition",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
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",
|