{"record":{"id":"d3faaccdf7deeef6","repo":"koala73/worldmonitor","slug":"exasearchadapter-does-not-support-single-product-p","errorCode":null,"errorMessage":"ExaSearchAdapter does not support single-product parsing","messagePattern":"ExaSearchAdapter does not support single-product parsing","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"consumer-prices-core/src/adapters/exa-search.ts","lineNumber":317,"sourceCode":"            inStock: true,\n            rawPayload: {\n              exaUrl: payload.firecrawlUrl,\n              firecrawlFallback: true,\n              basketSlug: payload.basketSlug,\n              itemCategory: payload.itemCategory,\n              canonicalName: payload.canonicalName,\n            },\n          },\n        ];\n      }\n      ctx.logger.warn(`  [firecrawl-fallback] ${payload.canonicalName}: no ${currency} price found in Firecrawl markdown either`);\n    }\n\n    return [];\n  }\n\n  async parseProduct(_ctx: AdapterContext, _result: FetchResult): Promise<ParsedProduct> {\n    throw new Error('ExaSearchAdapter does not support single-product parsing');\n  }\n\n  async validateConfig(config: RetailerConfig): Promise<string[]> {\n    const errors: string[] = [];\n    if (!this.apiKey) errors.push('EXA_API_KEY env var is required for adapter: exa-search');\n    if (!config.baseUrl) errors.push('baseUrl is required');\n    return errors;\n  }\n}\n","sourceCodeStart":299,"sourceCodeEnd":327,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/consumer-prices-core/src/adapters/exa-search.ts#L299-L327","documentation":"ExaSearchAdapter is a search-mode adapter: its fetchTarget() already produces parsed products straight from Exa results (with optional Firecrawl fallback extraction), so parseProduct() has nothing to do and throws unconditionally. Seeing this error means the pipeline treated the adapter like a page-scraping adapter (fetch then parse) — a protocol mismatch, not a data problem.","triggerScenarios":"A generic runner calling adapter.parseProduct(ctx, result) after fetchTarget() because the retailer config selected a scrape-style pipeline; harness code that round-trips every adapter through fetch+parse; new code assuming RetailerAdapter always implements parseProduct.","commonSituations":"Adding exa-search to a retailer whose pipeline stage list still includes a parse step; test suites iterating all adapters with a universal fetch-then-parse contract; a refactor moving parsing into a shared step without checking adapter capability.","solutions":["Remove the parse step for retailers using exa-search — products arrive already parsed from fetchTarget","Dispatch on adapter capability: search-mode adapters skip parseProduct entirely","Add an explicit adapter kind ('search' vs 'scrape') to the contract so runners branch on it instead of discovering this by exception"],"exampleFix":"// before — assumes every adapter is fetch-then-parse\nconst result = await adapter.fetchTarget(ctx, target);\nconst product = await adapter.parseProduct(ctx, result);\n\n// after — branch on adapter kind\nconst result = await adapter.fetchTarget(ctx, target);\nconst product = adapter.key === 'exa-search'\n  ? productsFromSearchResult(result) // already parsed inside the search payload\n  : await adapter.parseProduct(ctx, result);","handlingStrategy":"type-guard","validationCode":"// when assembling pipeline stages\nfunction requiresParseStage(adapter: RetailerAdapter): boolean {\n  return adapter.key !== 'exa-search'; // search-mode adapters finish parsing in fetchTarget\n}\nif (requiresParseStage(adapter)) stages.push(parseStage);","typeGuard":"function isSearchModeAdapter(a: RetailerAdapter): boolean {\n  return a.key === 'exa-search'; // parseProduct() throws by design on this adapter\n}","tryCatchPattern":"try {\n  product = await adapter.parseProduct(ctx, result);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('does not support single-product parsing')) {\n    product = productsFromSearchResult(result); // search adapters embed parsed products in the payload\n  } else {\n    throw err;\n  }\n}","preventionTips":["Make adapter capability explicit in the contract (kind: 'search' | 'scrape') instead of relying on throws","Validate pipeline stage lists per retailer adapter at startup","In tests, exercise each adapter through its own supported protocol, not a universal fetch+parse loop"],"tags":["adapter","protocol-mismatch","search-mode","pipeline"],"backgroundTag":"unsupported-operation","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-23T16:17:53.355Z"}