{"record":{"id":"3468cc8eff9359b3","repo":"koala73/worldmonitor","slug":"generic-adapter-requires-acquisition-config-retai","errorCode":null,"errorMessage":"Generic adapter requires acquisition config (retailer: ${ctx.config.slug})","messagePattern":"Generic adapter requires acquisition config \\(retailer: (.+?)\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"consumer-prices-core/src/adapters/generic.ts","lineNumber":58,"sourceCode":"    return el?.getAttribute(attr.trim()) ?? null;\n  }\n\n  return doc.querySelector(selector)?.textContent?.trim() ?? null;\n}\n\nexport class GenericPlaywrightAdapter implements RetailerAdapter {\n  readonly key = 'generic';\n\n  async discoverTargets(ctx: AdapterContext): Promise<Target[]> {\n    return ctx.config.discovery.seeds.map((s) => ({\n      id: s.id,\n      url: s.url.startsWith('http') ? s.url : `${ctx.config.baseUrl}${s.url}`,\n      category: s.category ?? s.id,\n    }));\n  }\n\n  async fetchTarget(ctx: AdapterContext, target: Target): Promise<FetchResult> {\n    if (!ctx.config.acquisition) throw new Error(`Generic adapter requires acquisition config (retailer: ${ctx.config.slug})`);\n    const result = await fetchWithFallback(target.url, ctx.config.acquisition, ctx.config.rateLimit ? {\n      timeout: 30_000,\n    } : undefined);\n\n    return {\n      url: result.url,\n      html: result.html,\n      markdown: result.markdown,\n      statusCode: result.statusCode,\n      fetchedAt: result.fetchedAt,\n    };\n  }\n\n  async parseListing(ctx: AdapterContext, result: FetchResult): Promise<ParsedProduct[]> {\n    const selectors = ctx.config.extraction?.productCard;\n    if (!selectors) return [];\n\n    const dom = new JSDOM(result.html);","sourceCodeStart":40,"sourceCodeEnd":76,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/consumer-prices-core/src/adapters/generic.ts#L40-L76","documentation":"GenericPlaywrightAdapter.fetchTarget() delegates URL acquisition to fetchWithFallback(), which needs the retailer config's acquisition block (provider, options, optional fallback). The error fires deterministically when a retailer using the generic adapter has no acquisition field — the adapter has no other way to know which provider chain to use for its discovery seeds.","triggerScenarios":"A retailer config with adapter 'generic' and discovery.seeds but a missing or renamed acquisition section; acquisition nested at the wrong level after a config schema change; configs written for a different adapter being switched to generic without adding the field.","commonSituations":"Adding a new retailer from an old template that predates the acquisition field; a config schema migration renaming the block; hand-edited configs in a deploy repo drifting from the schema.","solutions":["Add an acquisition block to the retailer config: { provider: 'playwright'|'firecrawl'|'p0', options?, fallback? }","Validate retailer configs at load time (adapter-specific required fields) so this fails at startup, not per-target","Check for schema drift — the field must be exactly 'acquisition' on the retailer config object"],"exampleFix":"// before\n{ slug: 'retailer-x', baseUrl: 'https://x.example', adapter: 'generic', discovery: { seeds: [...] } }\n\n// after\n{\n  slug: 'retailer-x', baseUrl: 'https://x.example', adapter: 'generic',\n  discovery: { seeds: [...] },\n  acquisition: { provider: 'playwright', fallback: 'firecrawl' },\n}","handlingStrategy":"validation","validationCode":"function validateGenericConfig(cfg: RetailerConfig): string[] {\n  const errs: string[] = [];\n  if (cfg.adapter === 'generic' && !cfg.acquisition) {\n    errs.push(`retailer '${cfg.slug}': generic adapter requires an acquisition block`);\n  }\n  return errs;\n}\n// at startup\nconst errs = retailerConfigs.flatMap(validateGenericConfig);\nif (errs.length) throw new Error(errs.join('; '));","typeGuard":"function hasAcquisition(cfg: RetailerConfig): cfg is RetailerConfig & { acquisition: AcquisitionConfig } {\n  return cfg.acquisition !== undefined;\n}","tryCatchPattern":"try {\n  await adapter.fetchTarget(ctx, target);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('requires acquisition config')) {\n    skipRetailer(ctx.config.slug, 'misconfigured'); // permanent config bug: skip and alert, do not retry\n    return;\n  }\n  throw err;\n}","preventionTips":["Run a config lint pass at deploy time that checks adapter-specific required fields","Keep retailer config templates schema-generated so required fields cannot drift","Treat 'requires X config' errors as permanent: skip the retailer and alert instead of retrying"],"tags":["adapter","configuration","missing-field","retailer-config"],"backgroundTag":"config-validation-failed","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-23T16:17:53.355Z"}