import { CheerioCrawler, RequestQueue } from 'crawlee'; import * as path from 'node:path'; import * as fs from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { URL } from 'node:url'; import { execSync } from 'node:child_process'; import axios from 'axios'; import { FileCacheAdapter } from '../src/utils/cache/file-adapter.js'; import { initialState } from '../src/logic/pricing/constants.js'; async function main() { const OPENROUTER_KEY = process.env.OPENROUTER_KEY; if (!OPENROUTER_KEY) { console.error('❌ Error: OPENROUTER_KEY not found in environment.'); process.exit(1); } let briefing = ''; let targetUrl: string | null = null; let comments: string | null = null; let cacheKey: string | null = null; let jsonStatePath: string | null = null; const args = process.argv.slice(2); for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--url') { targetUrl = args[++i]; } else if (arg === '--comments' || arg === '--notes') { comments = args[++i]; } else if (arg === '--cache-key') { cacheKey = args[++i]; } else if (arg === '--json') { jsonStatePath = args[++i]; } else if (!arg.startsWith('--')) { briefing = arg; } } if (briefing && briefing.startsWith('@')) { const rawPath = briefing.substring(1); const filePath = rawPath.startsWith('/') ? rawPath : path.resolve(process.cwd(), rawPath); briefing = await fs.readFile(filePath, 'utf8'); } // Discovery ONLY if not provided if (!targetUrl && briefing) { const urlMatch = briefing.match(/https?:\/\/[^\s]+/); if (urlMatch) { targetUrl = urlMatch[0]; console.log(`🔗 Discovered URL in briefing: ${targetUrl}`); } } if (!briefing && !targetUrl && !comments && !jsonStatePath) { console.error('❌ Usage: npm run ai-estimate -- "Briefing text" [--url https://example.com] [--comments "Manual notes"]'); console.error(' Or: npm run ai-estimate -- @briefing.txt [--url https://example.com]'); console.error(' Or: npm run ai-estimate -- --json path/to/state.json'); process.exit(1); } const clearCache = process.argv.includes('--clear-cache'); if (clearCache) { console.log('🧹 Clearing cache...'); const cacheFiles = await fs.readdir(path.join(process.cwd(), '.cache')); for (const file of cacheFiles) { if (file.startsWith('ai_est_')) { await fs.unlink(path.join(process.cwd(), '.cache', file)); } } } const cache = new FileCacheAdapter({ prefix: 'ai_est_' }); const finalCacheKey = cacheKey || `${briefing}_${targetUrl}_${comments}`; // 1. Crawl if URL provided let crawlContext = ''; if (targetUrl) { console.log(`🔍 Crawling ${targetUrl} for context...`); const cachedCrawl = await cache.get(`crawl_${targetUrl}`); if (cachedCrawl && !clearCache) { console.log('📦 Using cached crawl results.'); crawlContext = cachedCrawl; } else { crawlContext = await performCrawl(targetUrl); await cache.set(`crawl_${targetUrl}`, crawlContext, 86400); // 24h cache } } // 2. Distill Crawl Context (Context Filtering) let distilledCrawl = ''; if (crawlContext) { const cachedDistilled = await cache.get(`distilled_${targetUrl}`); if (cachedDistilled && !clearCache) { distilledCrawl = cachedDistilled; } else { distilledCrawl = await distillCrawlContext(crawlContext, OPENROUTER_KEY); await cache.set(`distilled_${targetUrl}`, distilledCrawl, 86400); } } // 3. AI Prompting console.log('🤖 Consultating Gemini 3 Flash...'); const cachedAi = !clearCache ? await cache.get(finalCacheKey) : null; let formState: any; let usage: { prompt: number, completion: number, cost: number } = { prompt: 0, completion: 0, cost: 0 }; // Load Context Documents const principles = await fs.readFile(path.resolve(process.cwd(), 'docs/PRINCIPLES.md'), 'utf8'); const techStandards = await fs.readFile(path.resolve(process.cwd(), 'docs/TECH.md'), 'utf8'); const tone = await fs.readFile(path.resolve(process.cwd(), 'docs/TONE.md'), 'utf8'); if (jsonStatePath) { console.log(`📂 Loading state from JSON: ${jsonStatePath}`); const rawJson = await fs.readFile(path.resolve(process.cwd(), jsonStatePath), 'utf8'); formState = JSON.parse(rawJson); } else if (cachedAi) { console.log('📦 Using cached AI response.'); formState = cachedAi; } else { const result = await getAiEstimation(briefing, distilledCrawl, comments, OPENROUTER_KEY, principles, techStandards, tone); formState = result.state; usage = result.usage; await cache.set(finalCacheKey, formState); } // 3. Save Data & Generate PDF const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const jsonOutDir = path.resolve(process.cwd(), 'out/estimations/json'); if (!existsSync(jsonOutDir)) await fs.mkdir(jsonOutDir, { recursive: true }); const finalJsonPath = path.join(jsonOutDir, `${formState.companyName || 'unknown'}_${timestamp}.json`); await fs.writeFile(finalJsonPath, JSON.stringify(formState, null, 2)); const tempJsonPath = path.resolve(process.cwd(), '.cache', `temp_state_${Date.now()}.json`); await fs.writeFile(tempJsonPath, JSON.stringify(formState, null, 2)); console.log(`📦 Saved detailed state to: ${finalJsonPath}`); console.log('📄 Generating PDF estimation...'); try { execSync(`npx tsx ./scripts/generate-quote.ts --input ${tempJsonPath}`, { stdio: 'inherit' }); } finally { // await fs.unlink(tempJsonPath); } console.log('\n✨ AI Estimation Complete!'); if (usage.prompt > 0) { console.log('--------------------------------------------------'); console.log('📊 ACCUMULATED API USAGE (SUM OF 6 PASSES)'); console.log(` Model: google/gemini-3-flash-preview`); console.log(` Total Prompt: ${usage.prompt.toLocaleString()}`); console.log(` Total Completion: ${usage.completion.toLocaleString()}`); console.log(` Total Tokens: ${(usage.prompt + usage.completion).toLocaleString()}`); console.log(` Total Cost (USD): $${usage.cost.toFixed(6)}`); console.log('--------------------------------------------------\n'); } } async function distillCrawlContext(rawCrawl: string, apiKey: string): Promise { if (!rawCrawl || rawCrawl.trim().length === 0) return "Keine Crawl-Daten vorhanden."; console.log(' ↳ Distilling Crawl Context (Noise Filtering)...'); const systemPrompt = ` You are a context distiller. Your goal is to strip away HTML noise, legal footers, and generic fluff from a website crawl. Extract the "Company DNA" in 5-8 bullet points (GERMAN). ### FOCUS ON: 1. Core Business / Services. 2. Unique Selling Points (USPs). 3. Target Audience (if clear). 4. Tech Stack or industry-specific equipment mentioned. 5. Brand tone (e.g. "industrial", "friendly", "technical"). ### OUTPUT: Return ONLY the bullet points. No intro/outro. `; const resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { model: 'google/gemini-3-flash-preview', messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: `RAW_CRAWL:\n${rawCrawl.substring(0, 30000)}` }], }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); return resp.data.choices[0].message.content; } async function performCrawl(url: string): Promise { const pages: { url: string, content: string, type: string }[] = []; const origin = new URL(url).origin; const crawler = new CheerioCrawler({ maxRequestsPerCrawl: 20, async requestHandler({ $, request, enqueueLinks }) { const title = $('title').text(); const urlObj = new URL(request.url); const urlPath = urlObj.pathname.toLowerCase(); let type = 'other'; if (urlPath === '/' || urlPath === '') type = 'home'; else if (urlPath.includes('service') || urlPath.includes('leistung')) type = 'service'; else if (urlPath.includes('blog') || urlPath.includes('news') || urlPath.includes('aktuelles') || urlPath.includes('magazin')) type = 'blog'; else if (urlPath.includes('contact') || urlPath.includes('kontakt')) type = 'contact'; else if (urlPath.includes('job') || urlPath.includes('karriere') || urlPath.includes('career') || urlPath.includes('human-resources')) type = 'career'; else if (urlPath.includes('portfolio') || urlPath.includes('referenz') || urlPath.includes('projekt') || urlPath.includes('case-study')) type = 'portfolio'; else if (urlPath.includes('legal') || urlPath.includes('impressum') || urlPath.includes('datenschutz') || urlPath.includes('privacy')) type = 'legal'; const h1s = $('h1').map((_, el) => $(el).text()).get(); const navLinks = $('nav a').map((_, el) => $(el).text().trim()).get().filter(t => t.length > 0); const bodyText = $('body').text().replace(/\s+/g, ' ').substring(0, 50000); const html = $.html(); const hexColors = html.match(/#(?:[0-9a-fA-F]{3}){1,2}\b/g) || []; const uniqueColors = Array.from(new Set(hexColors)).slice(0, 5); pages.push({ url: request.url, type, content: `Title: ${title}\nType: ${type}\nHeadings: ${h1s.join(', ')}\nNav: ${navLinks.join(', ')}\nColors: ${uniqueColors.join(', ')}\nText: ${bodyText}` }); await enqueueLinks({ limit: 15, transformRequestFunction: (req) => { const reqUrl = new URL(req.url); if (reqUrl.origin !== origin) return false; // Skip assets if (reqUrl.pathname.match(/\.(pdf|zip|jpg|png|svg|webp)$/i)) return false; return req; } }); }, }); await crawler.run([url]); const typeCounts = pages.reduce((acc, p) => { acc[p.type] = (acc[p.type] || 0) + 1; return acc; }, {} as Record); let summary = `\nCrawl Summary: Identified ${pages.length} pages total on ${origin}.\n`; summary += Object.entries(typeCounts).map(([type, count]) => `- ${type}: ${count}`).join('\n') + '\n\n'; return summary + pages.map(p => `--- PAGE: ${p.url} ---\n${p.content}`).join('\n\n'); } async function getAiEstimation(briefing: string, distilledCrawl: string, comments: string | null, apiKey: string, principles: string, techStandards: string, tone: string) { let usage = { prompt: 0, completion: 0, cost: 0 }; const addUsage = (data: any) => { if (data?.usage) { usage.prompt += data.usage.prompt_tokens || 0; usage.completion += data.usage.completion_tokens || 0; // OpenRouter provides 'cost' field in USD (as per documentation) // If missing, we use a fallback calculation for transparency if (data.usage.cost !== undefined) { usage.cost += data.usage.cost; } else { // Fallback: Gemini 3 Flash Flash pricing (~$0.1 / 1M prompt, ~$0.4 / 1M completion) usage.cost += (data.usage.prompt_tokens || 0) * (0.1 / 1000000) + (data.usage.completion_tokens || 0) * (0.4 / 1000000); } } }; // 1. PASS 1: Fact Extraction (Briefing Sensor) console.log(' ↳ Pass 1: Fact Extraction (Briefing Sensor)...'); const pass1SystemPrompt = ` You are a precision sensor. Analyze the BRIEFING and extract ONLY the raw facts. Tone: Literal, non-interpretive. Output language: GERMAN (Strict). ### MISSION: Focus 100% on the BRIEFING text provided by the user. Use the DISTILLED_CRAWL only as background context for terms or company details. If there is a conflict, the BRIEFING is the absolute source of truth. ### OBJECTIVES: - Extract **companyName**: The full legal and brand name (e.g., "E-TIB GmbH"). Use signatures and crawl data. - Extract **personName**: The name of the primary human contact (e.g., "Danny Joseph"). **CRITICAL**: Check email signatures and "Mit freundlichen Grüßen" blocks. DO NOT use "Sie", "Firma" or generic terms if a name exists. - Extract **existingWebsite**: The primary URL mentioned in the briefing or signature (e.g., "www.e-tib.com"). - Extract **websiteTopic**: A short descriptor of the CORE BUSINESS (e.g., "Kabeltiefbau"). MAX 3 WORDS. - **isRelaunch**: Set to TRUE if the briefing mentions an existing website, a URL, or if the company is an established entity (e.g. "Gruppe", "GmbH seit 20XX"). Assume a presence exists that needs a modern "Zentrale Webpräsenz". - **CRITICAL LOGIC**: If a URL is mentioned, isRelaunch MUST be TRUE. - For all textual values (deadline, websiteTopic, targetAudience etc.): USE GERMAN. - **multilang**: ONLY if the briefing mentions multiple target languages (e.g., DE/EN). - **maps**: If "Google Maps" or location maps are mentioned or implicit (Contact page). - **CRITICAL**: Do NOT include "social" in apiSystems unless the user explicitly wants to SYNC/POST content to social media. "Existing social media links" are NOT apiSystems. - **CRITICAL**: "Video Player", "Cookie Banner", "Animations" are NOT features. They are visual/base. Do NOT map them to features. ### CATEGORY MAPPING (IDs ONLY): - **selectedPages**: [Home, About, Services, Contact, Landing, Legal] - **features**: [blog_news, products, jobs, refs, events] - **functions**: [search, filter, pdf, forms, members, calendar, multilang, chat] - **apiSystems**: [crm_erp, payment, marketing, ecommerce, maps, social, analytics] - **assets**: [existing_website, logo, styleguide, content_concept, media, icons, illustrations, fonts] ### OUTPUT FORMAT (Strict JSON): { "companyName": string, "companyAddress": string, "personName": string, "existingWebsite": string, "websiteTopic": string, "isRelaunch": boolean, "selectedPages": string[], "features": string[], "functions": string[], "apiSystems": string[], "assets": string[], "deadline": string (GERMAN), "targetAudience": "B2B" | "B2C" | "Internal" | string (GERMAN), "expectedAdjustments": "low" | "medium" | "high" | string (GERMAN), "employeeCount": "ca. 10+" | "ca. 50+" | "ca. 100+" | "ca. 250+" | "ca. 500+" | "ca. 1000+" } `; const pass1UserPrompt = `BRIEFING (TRUTH SOURCE):\n${briefing}\n\nCOMMENTS:\n${comments}\n\nDISTILLED_CRAWL (CONTEXT ONLY):\n${distilledCrawl}`; const p1Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { model: 'google/gemini-3-flash-preview', messages: [{ role: 'system', content: pass1SystemPrompt }, { role: 'user', content: pass1UserPrompt }], response_format: { type: 'json_object' } }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); addUsage(p1Resp.data); const facts = JSON.parse(p1Resp.data.choices[0].message.content); // 2. PASS 2: Feature Deep-Dive console.log(' ↳ Pass 2: Feature Deep-Dive...'); const pass2SystemPrompt = ` You are a detail-oriented Solution Architect. For EVERY item selected in Pass 1 (pages, features, functions, apiSystems), write a specific justification and technical scope. ### RULES: 1. **CONCRETE & SPECIFIC**: Do NOT say "Implementation of X". Say "Displaying X with Y filters". 2. **NO EFFECTS**: Do not mention "fade-ins", "animations" or "visual styling". Focus on FUNCTION. 3. **ABSOLUTE RULE**: EVERYTHING MUST BE GERMAN. 4. **TRANSPARENCY**: Explain exactly what the USER gets. 5. **API NOTE**: For 'media' or 'video', explicitly state "Upload & Integration" (NO STREAMING). ### INPUT (from Pass 1): ${JSON.stringify(facts, null, 2)} ### OUTPUT FORMAT (Strict JSON): { "pageDetails": { "Home": string, ... }, "featureDetails": { "blog_news": string, ... }, "functionDetails": { "search": string, ... }, "apiDetails": { "crm_erp": string, ... } } `; const p2Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { model: 'google/gemini-3-flash-preview', messages: [{ role: 'system', content: pass2SystemPrompt }, { role: 'user', content: briefing }], response_format: { type: 'json_object' } }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); addUsage(p2Resp.data); const details = JSON.parse(p2Resp.data.choices[0].message.content); // 3. PASS 3: Strategic Content (Bespoke Strategy) console.log(' ↳ Pass 3: Strategic Content (Bespoke Strategy)...'); const pass3SystemPrompt = ` You are a high-end Digital Architect. Your goal is to make the CUSTOMER feel 100% understood. Analyze the BRIEFING and the EXISTING WEBSITE context. ### TONE & COMMUNICATION PRINCIPLES (STRICT): ${tone} ### OBJECTIVE: 1. **briefingSummary**: A deep, respectful summary of the status quo and the target state. - **LENGTH**: EXACTLY TWO PARAGRAPHS. Minimum 8 sentences total. - **MIRROR TEST**: Acknowledge the EXISTING website specifically. Why does the new project make sense NOW? - **ABSOLUTE RULE**: DO NOT claim "keine digitale Repräsentation", "erstmals abgebildet", "Erstplatzierung" or "kommunikative Lücke" regarding existence if Pass 1 identified this as a RELAUNCH (isRelaunch=true). EXPLICITLY acknowledge the existing context and the NEED FOR EVOLUTION/MODERNIZATION. - **RESPECT**: Explicitly incorporate the customer's expressed wishes. - **ABSOLUTE RULE**: DO NOT INVENT DETAILS. Do not mention specific people (e.g., "Frieder Helmich"), software versions, or internal details NOT present in the briefing. - **TONE**: Natural Ich-Form. Clear, direct, zero marketing fluff. 2. **designVision**: A high-density, professional vision of the future execution. - **LENGTH**: EXACTLY TWO PARAGRAPHS. Minimum 6 sentences total. - **INVESTMENT VALUE**: Plant a clear picture of a stable, high-quality system. - **TECHNICAL PRECISION**: Focus on execution (Typografie, Visual Logic, Performance). - **NO FLUFF**: Do NOT focus on "Full-Screen Hero Video" as the main thing. Focus on the FIRM's essence and how we translate it into a professional tool. - **ABSOLUTE RULE**: NO HALLUCINATIONS. Stay general yet precise. No "Verzicht auf Stockmaterial" unless explicitly stated. ### OUTPUT FORMAT (Strict JSON): { "briefingSummary": string, "designVision": string } `; const p3Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { model: 'google/gemini-3-flash-preview', messages: [ { role: 'system', content: pass3SystemPrompt }, { role: 'user', content: `BRIEFING (TRUTH SOURCE):\n${briefing}\n\nEXISTING WEBSITE (CONTEXT):\n${distilledCrawl}\n\nEXTRACTED FACTS:\n${JSON.stringify(facts, null, 2)}` } ], response_format: { type: 'json_object' } }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); addUsage(p3Resp.data); const strategy = JSON.parse(p3Resp.data.choices[0].message.content); // 4. PASS 4: Information Architecture (Sitemap) console.log(' ↳ Pass 4: Information Architecture...'); const pass4SystemPrompt = ` You are a Senior UX Architect. Design a hierarchical sitemap following the 'Industrial Logic' principle. EVERYTHING MUST BE IN GERMAN. ### SITEMAP RULES: 1. **HIERARCHY**: Build a logical tree. Group by category (e.g., "Kern-Präsenz", "Lösungen", "Vertrauen", "Rechtliches"). 2. **INTENT**: Each page MUST have a title and a brief functional conversion intent (desc). 3. **COMPREHENSIVENESS**: Ensure all 'selectedPages' and 'features' from Pass 1 are represented. 4. **LANGUAGE**: STRICT GERMAN TITLES. Do NOT use "Home", "About", "Services". Use "Startseite", "Über uns", "Leistungen". ### DATA CONTEXT: ${JSON.stringify({ facts, strategy }, null, 2)} ### OUTPUT FORMAT (Strict JSON): { "websiteTopic": string, "sitemap": [ { "category": string, "pages": [ { "title": string, "desc": string } ] } ] } `; const p4Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { model: 'google/gemini-3-flash-preview', messages: [{ role: 'system', content: pass4SystemPrompt }, { role: 'user', content: `BRIEFING (TRUTH SOURCE):\n${briefing}\n\nDISTILLED_CRAWL (CONTEXT):\n${distilledCrawl}` }], response_format: { type: 'json_object' } }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); addUsage(p4Resp.data); const ia = JSON.parse(p4Resp.data.choices[0].message.content); // 5. PASS 5: Position Synthesis & Pricing Transparency console.log(' ↳ Pass 5: Position Synthesis...'); const pass5SystemPrompt = ` You are a Senior Solution Architect. Your goal is ABSOLUTE TRANSPARENCY for the customer. Each position in the quote must be perfectly justified and detailed. ### POSITION TITLES: "Basis Website Setup", "Individuelle Seiten", "System-Module", "Logik-Funktionen", "Visuelle Inszenierung", "Komplexe Interaktion", "Schnittstellen (API)", "Inhaltsverwaltung (CMS)", "Sorglos-Betrieb (Hosting)". ### MAPPING RULES (STRICTLY BASED ON PRICING.MD): - **Basis Website Setup**: Infrastructure, Hosting, SEO-Basics, Cookie-Consent, Staging/Production. - **Individuelle Seiten**: Layout and structure for specific pages (e.g. Home, Services). - **System-Module (Features)**: Closed systems with data structures (Blog, News, Products, Jobs, References). - **Logik-Funktionen**: Pure logic (Search, Filter, Forms, PDF-Export, Multi-lang). - **Visuelle Inszenierung**: Hero-Story, visual flows, Scroll-effects. - **Komplexe Interaktion**: UI-experiences like Configurators or multi-step processes. - **Schnittstellen (API)**: REAL Data Syncs (CRM, ERP, Stripe). - **Inhaltsverwaltung (CMS)**: Setup and mapping for Headless CMS. - **Sorglos-Betrieb (Hosting)**: Hosting, Updates, Backups. ### RULES FOR positionDescriptions (STRICT): 1. **NO "ICH-FORM"**: Do NOT use "Ich" or "Mein". Lead with the action or component. 2. **CONCISE & ITEM-BASED**: Use short, technical sentences. Focus on WHAT is delivered. 3. **ZERO GENERALIZATION**: Do NOT say "Verschiedene Funktionen". 4. **ITEMIZED SYNTHESIS**: Mention EVERY component selected in Pass 1. 5. **HARD SPECIFICS**: Preserve technical details from the briefing (e.g., "110 kV", "HDD-Bohrtechnik"). 6. **STYLE**: Direct, engineering-grade, 100% GERMAN. 7. **SPECIFIC - PAGES**: For "Individuelle Seiten", list the pages as a comma-separated list. 8. **SPECIFIC - HOSTING**: Always append: "Inkl. 20GB Speicher." 9. **SPECIFIC - LOGIC**: Describe the ACTUAL logic. NEVER use generic terms. 10. **STRICT KEYS**: Keys MUST be EXACTLY the ones defined in POSITION TITLES. ### EXAMPLES (FEW-SHOT): - **BAD**: "Ich entwickle die Seiten: Startseite, Leistungen, Kontakt." - **GOOD**: "Erstellung der Seiten: Startseite (Video-Hero), Über uns (Timeline), Leistungen, Kontakt." - **BAD**: "Ich binde Google Maps an." - **GOOD**: "Native API-Integration von Google Maps mit individueller Standort-Visualisierung." - **BAD**: "Ich programmiere Scroll-Effekte." - **GOOD**: "Visuelle Inszenierung der Meilensteine durch Scroll-aktivierte Timeline-Elemente." ### POSITION RULES (STRICT): 1. **Basis Website Setup**: This position MUST ALWAYS contain exactly these 7 points: "Projekt-Setup & Infrastruktur, Hosting-Bereitstellung, Grundstruktur & Design-Vorlage, technisches SEO-Basics, Analytics (mit automatischem Mail-Report), Testing-, Staging- & Production-Umgebung, Livegang." 2. **Sorglos-Betrieb (Hosting)**: Describe the service (Hosting, SSL, Security-Updates, 24/7 Monitoring, Portfolio-Update-Service). NEVER mention "Inklusive Basis-Infrastruktur". 3. **LOGIC CONSISTENCY**: If Pass 1 extracted 1 function, you MUST describe exactly 1 function scope. If you describe two things (e.g., "Formular AND Search") but the count is 1, it is a FAIL. 4. **SIMPLICITY**: Write in "Simple German". High density of information, but easy for a CEO. No jargon. 5. **NO IMPLEMENTATION DETAILS**: Focus on WHAT is done, not HOW (no libraries, no technical "under-the-hood" talk). ### DATA CONTEXT: ${JSON.stringify({ facts, details, strategy, ia }, null, 2)} ### OUTPUT FORMAT (Strict JSON): { "positionDescriptions": { "Basis Website Setup": string, ... } } `; const p5Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { model: 'google/gemini-3-flash-preview', messages: [{ role: 'system', content: pass5SystemPrompt }, { role: 'user', content: briefing }], response_format: { type: 'json_object' } }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); addUsage(p5Resp.data); const positionsData = JSON.parse(p5Resp.data.choices[0].message.content); // 6. PASS 6: The Industrial Critic console.log(' ↳ Pass 6: The Industrial Critic (Quality Gate)...'); const pass6SystemPrompt = ` You are the "Industrial Critic". Your goal is to catch quality regressions and ensure the document is bespoke, technical, and professional. Analyze the CURRENT_STATE against the BRIEFING_TRUTH. ### CRITICAL ERROR CHECKLIST (FAIL IF FOUND): 1. **Hallucination Leakage**: FAIL if names of people (e.g., "Frieder Helmich"), specific software versions, or invented details are used unless they appear EXACTLY in the BRIEFING. - **CRITICAL**: Forbid "Sie", "Ansprechpartner" or "Unternehmen" for personName if a name IS in the briefing. If none is in briefing, use empty string. 2. **Logic Conflict**: FAIL if isRelaunch is true but briefingSummary claims no website exists or uses phrases like "Da aktuell keine digitale Repräsentation vorliegt", "erstmals abgebildet", "Erstplatzierung" or "Lücke schließen" (regarding existence). - FAIL if the description in positionDescriptions mentions more items than extracted in facts. 3. **Implementation Fluff**: FAIL if "React", "Next.js", "TypeScript", "Tailwind" or other tech-stack details are mentioned. Focus on Concept & Result. 4. **Length Check**: Briefing and Vision MUST be significantly long (EXACTLY 2 paragraphs each, minimum 8 sentences for briefing, 6 for vision). ### MISSION: Return updated fields ONLY. Specifically focus on hardening 'positionDescriptions', 'sitemap', 'briefingSummary', and 'designVision'. ### DATA CONTEXT: ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)} `; const p6Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { model: 'google/gemini-3-flash-preview', messages: [{ role: 'system', content: pass6SystemPrompt }, { role: 'user', content: `BRIEFING_TRUTH:\n${briefing}` }], response_format: { type: 'json_object' } }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); addUsage(p6Resp.data); const reflection = JSON.parse(p6Resp.data.choices[0].message.content); // 6. Reflection Merge Utility const mergeReflection = (state: any, reflection: any) => { let result = { ...state }; const unwrap = (obj: any): any => { if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj; if (obj["0"]) return unwrap(obj["0"]); if (obj.state) return unwrap(obj.state); if (obj.facts) return unwrap(obj.facts); if (obj.strategy) return unwrap(obj.strategy); if (obj.ia) return unwrap(obj.ia); if (obj.positionsData) return unwrap(obj.positionsData); return obj; }; const cleanedReflection = unwrap(reflection); Object.entries(cleanedReflection).forEach(([key, value]) => { if (value && value !== "" && value !== "null") { result[key] = value; } }); return result; }; let finalState = mergeReflection({ ...initialState, ...facts, ...strategy, ...ia, ...positionsData }, reflection); finalState.statusQuo = facts.isRelaunch ? 'Relaunch' : 'Neuentwicklung'; // Normalization Layer: Map hallucinated German keys back to internal keys const normalizationMap: Record = { "Briefing-Zusammenfassung": "briefingSummary", "Design-Vision": "designVision", "Zusammenfassung": "briefingSummary", "Vision": "designVision", "BRIEFING_SUMMARY": "briefingSummary", "DESIGN_VISION": "designVision" }; Object.entries(normalizationMap).forEach(([gerKey, intKey]) => { if (finalState[gerKey] && !finalState[intKey]) { if (typeof finalState[gerKey] === 'object' && !Array.isArray(finalState[gerKey])) { finalState[intKey] = Object.values(finalState[gerKey]).join('\n\n'); } else { finalState[intKey] = finalState[gerKey]; } } }); // Sitemap Normalization (German keys to internal) if (Array.isArray(finalState.sitemap)) { finalState.sitemap = finalState.sitemap.map((cat: any) => ({ category: cat.category || cat.kategorie || cat.Kategorie || cat.title || "Allgemein", pages: (cat.pages || cat.seiten || cat.Seiten || []).map((page: any) => ({ title: page.title || page.titel || page.Titel || "Seite", desc: page.desc || page.beschreibung || page.Beschreibung || page.description || "" })) })); } // Position Descriptions Normalization if (finalState.positionDescriptions) { const normalized: Record = {}; Object.entries(finalState.positionDescriptions).forEach(([key, value]) => { const normalizedKey = key === 'titel' || key === 'Title' ? 'title' : key; const normalizedValue = typeof value === 'object' ? (value as any).beschreibung || (value as any).description || JSON.stringify(value) : value; normalized[normalizedKey] = normalizedValue as string; }); finalState.positionDescriptions = normalized; } // Normalize final state if (Array.isArray(finalState.positionDescriptions)) { const normalized: Record = {}; finalState.positionDescriptions.forEach((item: any) => { const key = item.feature || item.id || item.title || item.pos; if (key) normalized[key] = item.description || item.desc; }); finalState.positionDescriptions = normalized; } if (finalState.sitemap && !Array.isArray(finalState.sitemap)) { if (finalState.sitemap.categories) finalState.sitemap = finalState.sitemap.categories; else if (finalState.sitemap.sitemap) finalState.sitemap = finalState.sitemap.sitemap; else { const entries = Object.entries(finalState.sitemap); if (entries.every(([_, v]) => Array.isArray(v))) { finalState.sitemap = entries.map(([category, pages]) => ({ category, pages })); } } } return { state: finalState, usage }; } main().catch(console.error);