From 948fdb635205824062dd2ebb99d910271b91b8d6 Mon Sep 17 00:00:00 2001 From: Kacper Date: Tue, 30 Dec 2025 15:36:17 +0100 Subject: [PATCH] refactor(server): Use shared JSON extractor in feature and plan parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update parseAndCreateFeatures and parsePlanResponse to use the shared extractJson/extractJsonWithArray utilities instead of manual regex parsing for more robust and consistent JSON extraction from AI responses. - parse-and-create-features.ts: Use extractJsonWithArray for features - generate-plan.ts: Use extractJson with requiredKey for backlog plans 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../app-spec/parse-and-create-features.ts | 32 ++++++++++++------- .../src/routes/backlog-plan/generate-plan.ts | 32 +++++++++---------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/apps/server/src/routes/app-spec/parse-and-create-features.ts b/apps/server/src/routes/app-spec/parse-and-create-features.ts index 364f64ad..78137a73 100644 --- a/apps/server/src/routes/app-spec/parse-and-create-features.ts +++ b/apps/server/src/routes/app-spec/parse-and-create-features.ts @@ -7,6 +7,7 @@ import * as secureFs from '../../lib/secure-fs.js'; import type { EventEmitter } from '../../lib/events.js'; import { createLogger } from '@automaker/utils'; import { getFeaturesDir } from '@automaker/platform'; +import { extractJsonWithArray } from '../../lib/json-extractor.js'; const logger = createLogger('SpecRegeneration'); @@ -22,23 +23,30 @@ export async function parseAndCreateFeatures( logger.info('========== END CONTENT =========='); try { - // Extract JSON from response - logger.info('Extracting JSON from response...'); - logger.info(`Looking for pattern: /{[\\s\\S]*"features"[\\s\\S]*}/`); - const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/); - if (!jsonMatch) { - logger.error('❌ No valid JSON found in response'); + // Extract JSON from response using shared utility + logger.info('Extracting JSON from response using extractJsonWithArray...'); + + interface FeaturesResponse { + features: Array<{ + id: string; + category?: string; + title: string; + description: string; + priority?: number; + complexity?: string; + dependencies?: string[]; + }>; + } + + const parsed = extractJsonWithArray(content, 'features', { logger }); + + if (!parsed || !parsed.features) { + logger.error('❌ No valid JSON with "features" array found in response'); logger.error('Full content received:'); logger.error(content); throw new Error('No valid JSON found in response'); } - logger.info(`JSON match found (${jsonMatch[0].length} chars)`); - logger.info('========== MATCHED JSON =========='); - logger.info(jsonMatch[0]); - logger.info('========== END MATCHED JSON =========='); - - const parsed = JSON.parse(jsonMatch[0]); logger.info(`Parsed ${parsed.features?.length || 0} features`); logger.info('Parsed features:', JSON.stringify(parsed.features, null, 2)); diff --git a/apps/server/src/routes/backlog-plan/generate-plan.ts b/apps/server/src/routes/backlog-plan/generate-plan.ts index 8ff58b68..678c085c 100644 --- a/apps/server/src/routes/backlog-plan/generate-plan.ts +++ b/apps/server/src/routes/backlog-plan/generate-plan.ts @@ -10,6 +10,7 @@ import type { Feature, BacklogPlanResult, BacklogChange, DependencyUpdate } from import { DEFAULT_PHASE_MODELS } from '@automaker/types'; import { FeatureLoader } from '../../services/feature-loader.js'; import { ProviderFactory } from '../../providers/provider-factory.js'; +import { extractJson } from '../../lib/json-extractor.js'; import { logger, setRunningState, getErrorMessage } from './common.js'; import type { SettingsService } from '../../services/settings-service.js'; import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js'; @@ -43,24 +44,23 @@ function formatFeaturesForPrompt(features: Feature[]): string { * Parse the AI response into a BacklogPlanResult */ function parsePlanResponse(response: string): BacklogPlanResult { - try { - // Try to extract JSON from the response - const jsonMatch = response.match(/```json\n?([\s\S]*?)\n?```/); - if (jsonMatch) { - return JSON.parse(jsonMatch[1]); - } + // Use shared JSON extraction utility for robust parsing + const parsed = extractJson(response, { + logger, + requiredKey: 'changes', + }); - // Try to parse the whole response as JSON - return JSON.parse(response); - } catch { - // If parsing fails, return an empty result - logger.warn('[BacklogPlan] Failed to parse AI response as JSON'); - return { - changes: [], - summary: 'Failed to parse AI response', - dependencyUpdates: [], - }; + if (parsed) { + return parsed; } + + // If parsing fails, return an empty result + logger.warn('[BacklogPlan] Failed to parse AI response as JSON'); + return { + changes: [], + summary: 'Failed to parse AI response', + dependencyUpdates: [], + }; } /**