{"record":{"id":"0a141ac2407bf1ea","repo":"abhigyanpatwari/GitNexus","slug":"batch-enrichment-failed-falling-back-to-heuristic","errorCode":null,"errorMessage":"Batch enrichment failed, falling back to heuristics:","messagePattern":"Batch enrichment failed, falling back to heuristics:","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"gitnexus/src/core/ingestion/cluster-enricher.ts","lineNumber":217,"sourceCode":"      const jsonMatch = response.match(/\\[[\\s\\S]*\\]/);\n      if (jsonMatch) {\n        const parsed = JSON.parse(jsonMatch[0]) as Array<{\n          id: string;\n          name: string;\n          keywords: string[];\n          description: string;\n        }>;\n\n        for (const item of parsed) {\n          enrichments.set(item.id, {\n            name: item.name,\n            keywords: item.keywords || [],\n            description: item.description || '',\n          });\n        }\n      }\n    } catch (error) {\n      logger.warn({ error }, 'Batch enrichment failed, falling back to heuristics:');\n      // Fallback for this batch\n      for (const community of batch) {\n        enrichments.set(community.id, {\n          name: community.heuristicLabel,\n          keywords: [],\n          description: '',\n        });\n      }\n    }\n  }\n\n  // Fill in any missing communities\n  for (const community of communities) {\n    if (!enrichments.has(community.id)) {\n      enrichments.set(community.id, {\n        name: community.heuristicLabel,\n        keywords: [],\n        description: '',","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/aac7515d2a8c50a1f8f923c6fb77218b333560d6/gitnexus/src/core/ingestion/cluster-enricher.ts#L199-L235","documentation":"Batch enrichment sends up to `batchSize` (default 5) cluster descriptions in one LLM prompt and expects a JSON array back. The whole batch — generate() call plus the regex extract and JSON.parse of the array — sits in one try/catch: any failure logs 'Batch enrichment failed, falling back to heuristics' and every community in that batch drops to its heuristicLabel. Unlike the single-cluster path, a malformed batch response (prose instead of JSON, truncated array, context-window overflow) also triggers this warning because JSON.parse happens inside the try.","triggerScenarios":"llmClient.generate rejecting (auth/network/rate limit), or the model returning a response whose array extraction fails: JSON.parse throwing on truncated output, the /\\[[\\s\\S]*\\]/ greedy match capturing invalid text, or a batch prompt exceeding the model context so the reply is cut off mid-array.","commonSituations":"Large repos where 5 clusters x 15 members produces prompts near the context limit; models that wrap answers in prose despite the 'Output JSON array' instruction; rate limits and provider outages affecting a whole batch at once.","solutions":["Inspect the logged { error } field to distinguish API failure (401/429/ETIMEDOUT) from SyntaxError (malformed model output)","For SyntaxError/truncation: lower batchSize (fewer clusters per prompt) so responses fit and stay well-formed","For API errors: fix credentials/endpoint or retry after the rate-limit window","Re-run enrichment — missing entries are backfilled with heuristics automatically"],"exampleFix":"// before: one bad item or a truncated array fails the whole batch\nconst parsed = JSON.parse(jsonMatch[0]) as Item[];\nfor (const item of parsed) enrichments.set(item.id, item);\n\n// after: validate per item, salvage the good ones\nfor (const raw of parsed) {\n  if (raw && typeof raw.id === 'string' && typeof raw.name === 'string') {\n    enrichments.set(raw.id, { name: raw.name, keywords: Array.isArray(raw.keywords) ? raw.keywords : [], description: typeof raw.description === 'string' ? raw.description : '' });\n  } // else: this id alone falls back to heuristic, batch survives\n}","handlingStrategy":"retry","validationCode":"// before each batch: cheap reachability probe; on failure retry once, then shrink the batch\nconst ok = await llmClient.generate('ping').then(() => true).catch(() => false);\nif (!ok) await backoffThenRetry(batch, /* smallerSize */ Math.max(1, batchSize - 2));","typeGuard":null,"tryCatchPattern":"try {\n  const parsed = JSON.parse(response.match(/\\[[\\s\\S]*\\]/)[0]) as Item[];\n  for (const raw of parsed) {\n    if (raw && typeof raw.id === 'string') enrichments.set(raw.id, normalizeItem(raw)); // per-item guard\n  }\n} catch (error) {\n  for (const community of batch) enrichments.set(community.id, heuristicFor(community)); // batch-level fallback\n}","preventionTips":["Keep batchSize small enough that 5 clusters x 15 members stays well inside the model context window","Ask for raw JSON output (no markdown fences) and validate per item so one bad entry does not sink the batch","Retry transient 429/5xx batches; heuristics only after a retry fails","Diff batch responses against the expected [{id, name, keywords, description}] shape in tests"],"tags":["llm","enrichment","batch","json-parse","clustering"],"backgroundTag":"llm-api-call-failed","analyzedSha":"aac7515d2a8c50a1f8f923c6fb77218b333560d6","analyzedAt":"2026-08-20T23:29:22.980Z","contentChangedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}