feat: Rename "Betrieb & Hosting" to "Sorglos-Paket" and refine PDF pricing module layout and text.
Some checks failed
Build & Deploy Mintel Blog / build-and-deploy (push) Failing after 2m28s

This commit is contained in:
2026-02-05 01:53:12 +01:00
parent 3e10bca3ac
commit c5ad6108ee
9 changed files with 502 additions and 277 deletions

View File

@@ -7,7 +7,8 @@ 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';
import { initialState, PRICING } from '../src/logic/pricing/constants.js';
import { calculateTotals } from '../src/logic/pricing/calculator.js';
async function main() {
const OPENROUTER_KEY = process.env.OPENROUTER_KEY;
@@ -19,6 +20,7 @@ async function main() {
let briefing = '';
let targetUrl: string | null = null;
let comments: string | null = null;
let budget: string | null = null;
let cacheKey: string | null = null;
let jsonStatePath: string | null = null;
@@ -32,6 +34,8 @@ async function main() {
targetUrl = args[++i];
} else if (arg === '--comments' || arg === '--notes') {
comments = args[++i];
} else if (arg === '--budget') {
budget = args[++i];
} else if (arg === '--cache-key') {
cacheKey = args[++i];
} else if (arg === '--json') {
@@ -77,7 +81,7 @@ async function main() {
}
const cache = new FileCacheAdapter({ prefix: 'ai_est_' });
const finalCacheKey = cacheKey || `${briefing}_${targetUrl}_${comments}`;
const finalCacheKey = cacheKey || `${briefing}_${targetUrl}_${comments}_${budget}`;
// 1. Crawl if URL provided
let crawlContext = '';
@@ -103,6 +107,11 @@ async function main() {
distilledCrawl = await distillCrawlContext(crawlContext, OPENROUTER_KEY);
await cache.set(`distilled_${targetUrl}`, distilledCrawl, 86400);
}
} else if (targetUrl) {
distilledCrawl = `WARNING: The crawl of ${targetUrl} failed (ENOTFOUND or timeout).
The AI must NOT hallucinate details about the current website.
Focus ONLY on the BRIEFING provided. If details are missing, mark them as 'unknown'.`;
console.warn('⚠️ Crawl failed. AI will be notified to avoid hallucinations.');
}
// 3. AI Prompting
@@ -124,7 +133,7 @@ async function main() {
console.log('📦 Using cached AI response.');
formState = cachedAi;
} else {
const result = await getAiEstimation(briefing, distilledCrawl, comments, OPENROUTER_KEY, principles, techStandards, tone);
const result = await getAiEstimation(briefing, distilledCrawl, comments, budget, OPENROUTER_KEY, principles, techStandards, tone);
formState = result.state;
usage = result.usage;
await cache.set(finalCacheKey, formState);
@@ -251,14 +260,24 @@ async function performCrawl(url: string): Promise<string> {
const cleanJson = (str: string) => {
// Remove markdown code blocks if present
let cleaned = str.replace(/```json\n?|```/g, '').trim();
// Remove potential control characters that break JSON.parse
cleaned = cleaned.replace(/[\u0000-\u001F\u007F-\u009F]/g, " ");
// Remove trailing commas before closing braces/brackets
// We keep \n \r \t for now as they might be escaped or need handling
cleaned = cleaned.replace(/[\u0000-\u0009\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, " ");
// Specific fix for Gemini: raw newlines inside strings
// This is tricky. We'll try to escape newlines that are NOT followed by a quote and colon (property start)
// or a closing brace/bracket. This is heuristic.
// A better way is to replace all raw newlines that are preceded by a non-backslash with \n
// but only if they are inside double quotes.
// Simplest robust approach: Remove trailing commas and hope response_format does its job.
cleaned = cleaned.replace(/,\s*([\]}])/g, '$1');
return cleaned;
};
const getAiEstimation = async (briefing: string, distilledCrawl: string, comments: string | null, apiKey: string, principles: string, techStandards: string, tone: string) => {
const getAiEstimation = async (briefing: string, distilledCrawl: string, comments: string | null, budget: 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) {
@@ -281,90 +300,150 @@ const getAiEstimation = async (briefing: string, distilledCrawl: string, comment
You are a precision sensor. Analyze the BRIEFING and extract ONLY the raw facts.
Tone: Literal, non-interpretive.
Output language: GERMAN (Strict).
Output format: ROOT LEVEL JSON (No wrapper keys like '0' or 'data').
### 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.
### PRICING REFERENCE (FOR CALCULATION):
- Base Project (Infrastructure + 12 Months Hosting): 5.440 € (MANDATORY START)
- Additional Pages: 600 € / stk
- System-Modules (Features): 1.500 € / stk
- Logic-Functions: 800 € / stk
- API Integrations: 800 € / stk
- CMS Setup: 1.500 € (optional)
- Visual Staging/Interactions: 1.500 € - 2.000 €
### 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]
${budget ? `### BUDGET LOGIC (ULTRA-STRICT):
1. **Mental Calculation**: Start with 7.000 €. Add items based on the reference above.
2. **Hard Ceiling**: If total > ${budget}, you MUST discard lower priority items.
3. **Priority**: High-End Design and Core Pages > Features.
4. **Restriction**: For ${budget}, do NOT exceed 2 features and 4 extra pages.
5. THE TOTAL COST CALCULATED BY THESE RULES MUST BE <= ${budget}.
6. Do NOT mention the budget in any string fields.` : ''}
### OUTPUT FORMAT (Strict JSON):
- ** features **: Items from the FEATURE_REFERENCE.
- ** ABSOLUTE CONSERVATIVE RULE **: Only use features if the briefing implies *dynamic complexity* (CMS, filtering, search, database).
- Simple keywords like 'Karriere', 'Referenzen', 'Messen' or lists of items MUST be treated as simple pages. Add them to 'otherPages' instead.
- If in doubt, categorizing as a PAGE is the mandatory default.
- ** otherPages **: Any specific pages mentioned (e.g. 'Historie', 'Team', 'Partner') that are not in the standard list. Use this for static lists of jobs or references too.
- ** companyName **: The full legal and brand name (e.g., "E-TIB GmbH"). Use signatures and crawl data.
- ** personName **: The name of the primary human contact (e.g., "Danny Joseph"). ** CRITICAL **: Check email signatures and "Mit freundlichen Grüßen" blocks.
- ** email **: The email address of the contact person if found in the briefing / signature.
- ** existingWebsite **: The primary URL mentioned in the briefing or signature (e.g., "www.e-tib.com").
- ** 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.
- ** CRITICAL LOGIC **: If a URL is mentioned, isRelaunch MUST be TRUE.
- For all textual values: USE GERMAN.
- ** multilang **: ONLY if the briefing mentions multiple target languages.
- ** maps **: If "Google Maps" or location maps are mentioned or implicit.
- ** CRITICAL **: Do NOT include "social" in apiSystems unless the user explicitly wants to SYNC / POST content.
### 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 - ROOT LEVEL):
{
"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+"
"companyName": string,
"companyAddress": string,
"personName": string,
"email": 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 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' } });
}, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } });
if (!p1Resp.data.choices?.[0]?.message?.content) {
console.error('❌ Pass 1 failed. Response:', JSON.stringify(p1Resp.data, null, 2));
throw new Error('Pass 1: No content in response');
}
const facts = JSON.parse(cleanJson(p1Resp.data.choices[0].message.content));
// 1.5. PASS 1.5: The Feature Auditor (Skeptical Review)
console.log(' ↳ Pass 1.5: The Feature Auditor (Skeptical Review)...');
const pass15SystemPrompt = `
You are a "Strict Cost Controller". Your mission is to prevent over-billing.
Review the extracted FEATURES and the BRIEFING.
### RULE OF THUMB:
- A "Feature" (1.500 €) is ONLY justified for complex, dynamic systems (logic, database, CMS-driven management, advanced filtering).
- Simple lists, information sections, or static descriptions (e.g., "Messen", "Team", "Historie", "Jobs" as mere text) are ALWAYS "Pages" (600 €).
- If the briefing doesn't explicitly mention "Management System", "Filterable Database", or "Client Login", it is likely a PAGE.
### MISSION:
Analyze each feature in the list. Decide if it should stay a "Feature" or be downgraded to the "otherPages" array.
### OUTPUT FORMAT:
Return only the corrected 'features' and 'otherPages' arrays.
{
"features": string[],
"otherPages": string[]
}
`;
const p15Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview',
messages: [
{ role: 'system', content: pass15SystemPrompt },
{ role: 'user', content: `EXTRACTED_FEATURES: ${JSON.stringify(facts.features)} \nBRIEFING: \n${briefing}` }
],
response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
addUsage(p15Resp.data);
const auditResult = JSON.parse(cleanJson(p15Resp.data.choices[0].message.content));
// Apply Audit: Downgrade features to otherPages
facts.features = auditResult.features || [];
facts.otherPages = Array.from(new Set([...(facts.otherPages || []), ...(auditResult.otherPages || [])]));
// 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.
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).
1. ** CONCRETE & SPECIFIC **: Do NOT say "Implementation of X". Say "Displaying X with Y filters".
2. ** JUSTIFICATION (CRITICAL) **: For every entry in 'featureDetails', explicitly explain WHY this is a complex system (1.500 €) and not just a static page (600 €). If it's just a list of items, it's a PAGE.
3. ** NO EFFECTS **: Do not mention "fade-ins", "animations" or "visual styling". Focus on FUNCTION.
4. ** ABSOLUTE RULE **: EVERYTHING MUST BE GERMAN.
5. ** TRANSPARENCY **: Explain exactly what the USER gets.
6. ** 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):
### OUTPUT FORMAT(Strict JSON):
{
"pageDetails": { "Home": string, ... },
"featureDetails": { "blog_news": string, ... },
"functionDetails": { "search": string, ... },
"apiDetails": { "crm_erp": string, ... }
"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' } });
}, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } });
addUsage(p2Resp.data);
if (!p2Resp.data.choices?.[0]?.message?.content) {
console.error('❌ Pass 2 failed. Response:', JSON.stringify(p2Resp.data, null, 2));
@@ -375,39 +454,39 @@ ${JSON.stringify(facts, null, 2)}
// 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.
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 & COMMUNICATION PRINCIPLES(STRICT):
${tone}
### OBJECTIVE:
3. **briefingSummary**: Ein sachlicher, tiefgehender Überblick der Unternehmenslage.
- **STIL**: Keine Ich-Form. Keine Marketing-Floskeln. Nutze präzise Fachbegriffe. Sei prägnant und effizient (ca. 70% der vorherigen Länge).
- **FORM**: EXAKT ZWEI ABSÄTZE. Insgesamt ca. 6 Sätze.
- **INHALT**: Welcher technologische Sprung ist notwendig? Was ist der Status Quo? (Bezug zur URL/Briefing).
- **ABSOLUTE REGEL**: Keine Halluzinationen über fehlende Präsenzen bei Relaunches.
- **DATENSCHUTZ**: KEINERLEI namentliche Nennungen von Personen (z. B. "Danny Joseph") in diesen Texten.
4. **designVision**: Ein abstraktes, strategisches Konzept.
- **STIL**: Rein konzeptionell. Keine Umsetzungsschritte. Keinerlei "To-dos". Keine Ich-Form. Sei prägnant.
- **FORM**: EXAKT ZWEI ABSÄTZE. Insgesamt ca. 4 Sätze.
- **DATENSCHUTZ**: KEINERLEI namentliche Nennungen von Personen in diesen Texten.
- **FOKUS**: Welche strategische Wirkung soll erzielt werden? (Z. B. "Industrielle Souveränität").
3. ** briefingSummary **: Ein sachlicher, tiefgehender Überblick der Unternehmenslage.
- ** STIL **: Keine Ich - Form.Keine Marketing - Floskeln.Nutze präzise Fachbegriffe.Sei prägnant und effizient(ca. 70 % der vorherigen Länge).
- ** FORM **: EXAKT ZWEI ABSÄTZE.Insgesamt ca. 6 Sätze.
- ** INHALT **: Welcher technologische Sprung ist notwendig ? Was ist der Status Quo ? (Bezug zur URL / Briefing).
- ** ABSOLUTE REGEL **: Keine Halluzinationen über fehlende Präsenzen bei Relaunches.
- ** DATENSCHUTZ **: KEINERLEI namentliche Nennungen von Personen(z.B. "Danny Joseph") in diesen Texten.
4. ** designVision **: Ein abstraktes, strategisches Konzept.
- ** STIL **: Rein konzeptionell.Keine Umsetzungsschritte.Keinerlei "To-dos".Keine Ich - Form.Sei prägnant.
- ** FORM **: EXAKT ZWEI ABSÄTZE.Insgesamt ca. 4 Sätze.
- ** DATENSCHUTZ **: KEINERLEI namentliche Nennungen von Personen in diesen Texten.
- ** FOKUS **: Welche strategische Wirkung soll erzielt werden ? (Z.B. "Industrielle Souveränität").
### OUTPUT FORMAT (Strict JSON):
### OUTPUT FORMAT(Strict JSON):
{
"briefingSummary": string,
"designVision": string
"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)}` }
{ 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' } });
}, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } });
addUsage(p3Resp.data);
if (!p3Resp.data.choices?.[0]?.message?.content) {
console.error('❌ Pass 3 failed. Response:', JSON.stringify(p3Resp.data, null, 2));
@@ -418,29 +497,30 @@ ${tone}
// 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.
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".
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".
5. ** NO IMPLEMENTATION NOTES **: Do NOT add implementation details in parentheses to titles (e.g. NO "Startseite (Hero-Video)", NO "About (Timeline)"). Keep titles clean and abstract.
### DATA CONTEXT:
${JSON.stringify({ facts, strategy }, null, 2)}
### OUTPUT FORMAT (Strict JSON):
### OUTPUT FORMAT(Strict JSON):
{
"websiteTopic": string,
"sitemap": [ { "category": string, "pages": [ { "title": string, "desc": string } ] } ]
"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}` }],
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' } });
}, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } });
addUsage(p4Resp.data);
if (!p4Resp.data.choices?.[0]?.message?.content) {
console.error('❌ Pass 4 failed. Response:', JSON.stringify(p4Resp.data, null, 2));
@@ -450,57 +530,75 @@ ${JSON.stringify({ facts, strategy }, null, 2)}
// 5. PASS 5: Position Synthesis & Pricing Transparency
console.log(' ↳ Pass 5: Position Synthesis...');
// Determine which positions are actually relevant to avoid hallucinations
const requiredPositions = [
"1. Das technische Fundament",
facts.selectedPages.length + facts.otherPages.length > 0 ? "2. Individuelle Seiten" : null,
facts.features.length > 0 ? "3. System-Module (Features)" : null,
facts.functions.length > 0 ? "4. Logik-Funktionen" : null,
facts.apiSystems.length > 0 ? "5. Schnittstellen (API)" : null,
facts.cmsSetup ? "6. Inhaltsverwaltung (CMS)" : null,
"7. Inszenierung & Interaktion", // Always include for high-end strategy
facts.multilang ? "8. Mehrsprachigkeit" : null,
"9. Inhaltliche Initial-Pflege",
"10. Sorglos-Paket (Betrieb & Pflege)"
].filter(Boolean);
const pass5SystemPrompt = `
You are a Senior Solution Architect. Your goal is ABSOLUTE TRANSPARENCY and professionalism.
Each position in the quote must be perfectly justified and detailed using an objective, technical tone.
### POSITION TITLES (STRICT - MUST MATCH EXACTLY):
"1. Das technische Fundament", "2. Individuelle Seiten", "3. System-Module (Features)", "4. Logik-Funktionen", "5. Schnittstellen (API)", "6. Inhaltsverwaltung (CMS)", "7. Inszenierung & Interaktion", "8. Mehrsprachigkeit", "9. Inhaltliche Initial-Pflege", "10. Laufender Betrieb & Hosting".
### REQUIRED POSITION TITLES (STRICT - ONLY DESCRIBE THESE):
${requiredPositions.map(p => `"${p}"`).join(", ")}
### MAPPING RULES (STRICT):
- **1. Das technische Fundament**: Infrastructure, Hosting setup, SEO-Basics, Analytics, Environments.
- **2. Individuelle Seiten**: Layout/structure for specific pages (Home, About, etc.).
- **3. System-Module (Features)**: Functional systems like Blog, News, Products, Jobs, References.
- **4. Logik-Funktionen**: Logic modules like Search, Filter, Forms, PDF-Export.
- **5. Schnittstellen (API)**: Data Syncs with CRM, ERP, Payment systems.
- **6. Inhaltsverwaltung (CMS)**: Setup and mapping for CMS.
- **7. Inszenierung & Interaktion**: Hero-stories, visual effects, configurators.
- **8. Mehrsprachigkeit**: Architecture scaling for multiple languages.
- **9. Inhaltliche Initial-Pflege**: Manual data entry/cleanup.
- **10. Laufender Betrieb & Hosting**: Ongoing maintenance, updates, 24/7 monitoring.
- ** 1. Das technische Fundament **: Infrastructure, Hosting setup, SEO-Basics, Analytics, Environments.
- ** 2. Individuelle Seiten **: Layout / structure for specific pages. ** RULE **: If quantity is high (e.g. > 10), lead with "Umsetzung von [QTY] individuellen Einzelseiten...".
- ** 3. System-Module (Features) **: Functional systems like Blog, News, Products, Jobs, References. ** RULE **: Describe exactly 1 thing if qty is 1. If qty is 0, DO NOT DESCRIBE THIS.
- ** 4. Logik-Funktionen **: Logic modules like Search, Filter, Forms, PDF-Export.
- ** 5. Schnittstellen (API) **: Data Syncs with CRM, ERP, Payment systems.
- ** 6. Inhaltsverwaltung (CMS) **: Setup and mapping for CMS.
- ** 7. Inszenierung & Interaktion **: Hero-stories, visual effects, configurators.
- ** 8. Mehrsprachigkeit **: Architecture scaling for multiple languages.
- ** 9. Inhaltliche Initial-Pflege **: Manual data entry / cleanup.
- ** 10. Sorglos-Paket (Betrieb & Pflege) **: ** RULE **: Describe as "1 Jahr Sicherung des technischen Betriebs, Instandhaltung, Sicherheits-Updates und Inhalts-Aktualisierungen gemäß AGB Punkt 7a."
### RULES FOR positionDescriptions (STRICT):
1. **ABSOLUTE RULE: NO FIRST PERSON**: NEVER use "Ich", "Mein", "Wir" or "Unser". Lead with nouns or passive verbs.
2. **PROFESSIONAL TONE**: Use "Erstellung von...", "Anbindung der...", "Implementierung technischer...", "Bereitstellung von...".
3. **CONCISE & ITEM-BASED**: Use technical, high-density sentences. Name specific industry terms from context.
4. **ITEMIZED SYNTHESIS**: Mention EVERY component selected in Pass 1.
5. **HARD SPECIFICS**: If the briefing mentions "Glasfaser-Trassen" or "Schwerlast-Logistik", IT MUST BE IN THE DESCRIPTION.
6. **INDUSTRIAL AMBITION**: Describe it as a high-end technical solution. Avoid "schöne Website" or marketing fluff.
7. **PAGES**: For "2. Individuelle Seiten", list the pages (e.g., "Startseite (Hero-Video), Leistungen, Kontakt").
8. **LOGIC**: Describe the ACTUAL logic (e.g., "Volltextsuche mit Auto-Complete", not "eine Suche").
9. **KEYS**: Return EXACTLY the keys defined in "POSITION TITLES".
10. **NO AGB**: NEVER mention "AGB" or "Geschäftsbedingungen".
### RULES FOR positionDescriptions(STRICT):
1. ** ABSOLUTE RULE: NO FIRST PERSON **: NEVER use "Ich", "Mein", "Wir" or "Unser". Lead with nouns or passive verbs.
2. ** QUANTITY PARITY (ULTRA-STRICT) **: The description MUST list EXACTLY the number of items matching the 'qty' for that position. If qty is 3, describe exactly 3 items. If qty is 1, describe exactly 1 item. Do NOT "stuff" additional features into one description.
3. ** LOGIC GUARD (CMS) **: If 'cmsSetup' is false in the DATA CONTEXT, you MUST NOT mention "CMS", "Modul", "Management System" or "Inhaltsverwaltung". Use "Statische Seite" or "Darstellung".
4. ** STATIC vs DYNAMIC **: If no complex logic was extracted in Pass 2 for a feature, describe it as a technical layout/page, not as a system.
5. ** PROFESSIONAL TONE **: Use "Erstellung von...", "Anbindung der...", "Implementierung technischer...", "Bereitstellung von...".
6. ** CONCISE & ITEM-BASED **: Use technical, high-density sentences. Name specific industry terms from context.
7. ** ITEMIZED SYNTHESIS **: Mention EVERY component selected in Pass 1.
8. ** HARD SPECIFICS **: If the briefing mentions "Glasfaser-Trassen" or "Schwerlast-Logistik", IT MUST BE IN THE DESCRIPTION.
9. ** INDUSTRIAL AMBITION **: Describe it as a high-end technical solution. Avoid "schöne Website" or marketing fluff.
10. ** PAGES **: For "2. Individuelle Seiten", list the pages. ** ABSOLUTE RULE **: Do NOT add implementation details or technical notes in parentheses (e.g. NO "(Matrix-Struktur)", NO "(Timeline-Modul)"). Use clean titles like "Startseite, Über uns, Leistungen".
11. ** LOGIC **: Describe the ACTUAL logic (e.g., "Volltextsuche mit Auto-Complete", not "eine Suche").
12. ** KEYS **: Return EXACTLY the keys defined in "POSITION TITLES".
13. ** NO AGB **: NEVER mention "AGB" or "Geschäftsbedingungen".
### EXAMPLES (PASSIVE & TECHNICAL):
- **GOOD**: "Konfiguration der CMS-Infrastruktur zur unabhängigen Verwaltung von Produkt-Katalogen und News-Beiträgen."
- **GOOD**: "Implementierung einer Volltextsuche inkl. Kategorisierungs-Logik für effizientes Auffinden von Projektreferenzen."
- **GOOD**: "Native API-Anbindung an das ERP-System zur Echtzeit-Synchronisation von Bestandsdaten."
- **BAD**: "Ich richte dir das CMS ein."
- **BAD**: "Ich programmiere eine tolle Suche für deine Seite."
### EXAMPLES(PASSIVE & TECHNICAL):
- ** GOOD **: "Konfiguration der CMS-Infrastruktur zur unabhängigen Verwaltung von Produkt-Katalogen und News-Beiträgen."
- ** GOOD **: "Implementierung einer Volltextsuche inkl. Kategorisierungs-Logik für effizientes Auffinden von Projektreferenzen."
- ** GOOD **: "Native API-Anbindung an das ERP-System zur Echtzeit-Synchronisation von Bestandsdaten."
- ** BAD **: "Ich richte dir das CMS ein."
- ** BAD **: "Ich programmiere eine tolle Suche für deine Seite."
### DATA CONTEXT:
${JSON.stringify({ facts, details, strategy, ia }, null, 2)}
### OUTPUT FORMAT (Strict JSON):
### OUTPUT FORMAT(Strict JSON):
{
"positionDescriptions": { "1. Das technische Fundament": string, ... }
"positionDescriptions": { "1. Das technische Fundament": 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' } });
}, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } });
addUsage(p5Resp.data);
if (!p5Resp.data.choices?.[0]?.message?.content) {
console.error('❌ Pass 5 failed. Response:', JSON.stringify(p5Resp.data, null, 2));
@@ -511,31 +609,32 @@ ${JSON.stringify({ facts, details, strategy, ia }, null, 2)}
// 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.
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.
### 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.
- FAIL if the description in positionDescriptions mentions more items than extracted in facts.
3. **Implementation Fluff**: FAIL if tech-stack details are mentioned (React, etc.). Focus on Concept & Result.
4. **Genericism Check (CRITICAL)**: FAIL if any text sounds like it could apply to ANY company. It MUST mention specific industry details (e.g., "Kabeltiefbau", "Infrastruktur-Zentrum") from the Briefing or Crawl.
6. **Namen-Verbot (STRICT)**: FAIL if any personal names (e.g. "Danny Joseph", "Joseph", etc.) appear in 'briefingSummary' or 'designVision'. Use abstract terms like "Unternehmensführung" or "Management" if necessary.
7. **AGB BAN**: FAIL if "Allgemeine Geschäftsbedingungen" or "AGB" appear anywhere.
8. **Length Check**: Briefing (ca. 6 Sätze) und Vision (ca. 4 Sätze). Kürze Texte, die zu ausschweifend sind, auf das Wesentliche.
3. ** Implementation Fluff **: FAIL if tech - stack details are mentioned(React, etc.).Focus on Concept & Result.
4. ** Genericism Check(CRITICAL) **: FAIL if any text sounds like it could apply to ANY company.It MUST mention specific industry details(e.g., "Kabeltiefbau", "Infrastruktur-Zentrum") from the Briefing or Crawl.
6. ** Namen-Verbot (STRICT) **: FAIL if any personal names (e.g. "Danny Joseph", "Joseph", etc.) appear in 'briefingSummary' or 'designVision'. Use abstract terms like "Unternehmensführung" or "Management" if necessary.
7. ** LOGIC GUARD (CMS) **: If 'cmsSetup' is false in the DATA CONTEXT, FAIL if any 'positionDescriptions' or 'briefingSummary' mentions "CMS", "Content Management System" or "Inhaltsverwaltung".
8. ** AGB BAN **: FAIL if "Allgemeine Geschäftsbedingungen" or "AGB" appear anywhere.
9. ** Length Check **: Briefing (ca. 6 Sätze) und Vision (ca. 4 Sätze). Kürze Texte, die zu ausschweifend sind, auf das Wesentliche.
### MISSION:
Return updated fields ONLY. Specifically focus on hardening 'positionDescriptions', 'sitemap', 'briefingSummary', and 'designVision'.
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}` }],
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' } });
}, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } });
addUsage(p6Resp.data);
if (!p6Resp.data.choices?.[0]?.message?.content) {
console.error('❌ Pass 6 failed. Response:', JSON.stringify(p6Resp.data, null, 2));
@@ -548,12 +647,10 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)}
let result = { ...state };
const unwrap = (obj: any): any => {
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj;
// Always unwrap "0" if it exists, regardless of other keys (AI often nests)
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);
if (obj.state && Object.keys(obj).length === 1) return unwrap(obj.state);
if (obj.facts && Object.keys(obj).length === 1) return unwrap(obj.facts);
return obj;
};
@@ -576,6 +673,10 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)}
finalState.statusQuo = facts.isRelaunch ? 'Relaunch' : 'Neuentwicklung';
// Recipient Mapping
if (finalState.personName) finalState.name = finalState.personName;
if (finalState.email) finalState.email = finalState.email;
// Normalization Layer: Map hallucinated German keys back to internal keys
const normalizationMap: Record<string, string> = {
"Briefing-Zusammenfassung": "briefingSummary",
@@ -596,6 +697,27 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)}
}
});
// Final Logic Guard: Strictly strip CMS from ALL descriptions and fields if not enabled
if (!finalState.cmsSetup) {
const stripCMS = (obj: any): any => {
if (typeof obj === 'string') {
return obj.replace(/CMS|Content-Management-System|Inhaltsverwaltung/gi, 'Plattform-Struktur');
}
if (Array.isArray(obj)) {
return obj.map(stripCMS);
}
if (obj !== null && typeof obj === 'object') {
const newObj: any = {};
Object.entries(obj).forEach(([k, v]) => {
newObj[k] = stripCMS(v);
});
return newObj;
}
return obj;
};
finalState = stripCMS(finalState);
}
// Sitemap Normalization (German keys to internal)
if (Array.isArray(finalState.sitemap)) {
finalState.sitemap = finalState.sitemap.map((cat: any) => ({
@@ -607,14 +729,41 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)}
}));
}
// Position Descriptions Normalization
// Position Descriptions Normalization (Strict Title Mapping + Index-based Fallback)
if (finalState.positionDescriptions) {
const normalized: Record<string, string> = {};
Object.entries(finalState.positionDescriptions).forEach(([key, value]) => {
const normalizedKey = key === 'titel' || key === 'Title' ? 'title' : key;
const rawPositions = finalState.positionDescriptions;
// 1. Initial cleanup
Object.entries(rawPositions).forEach(([key, value]) => {
const normalizedValue = typeof value === 'object' ? (value as any).beschreibung || (value as any).description || JSON.stringify(value) : value;
normalized[normalizedKey] = normalizedValue as string;
normalized[key] = normalizedValue as string;
});
// 2. Index-based matching (Map "10. Foo" to "10. Bar")
const standardTitles = [
"1. Das technische Fundament",
"2. Individuelle Seiten",
"3. System-Module (Features)",
"4. Logik-Funktionen",
"5. Schnittstellen (API)",
"6. Inhaltsverwaltung (CMS)",
"7. Inszenierung & Interaktion",
"8. Mehrsprachigkeit",
"9. Inhaltliche Initial-Pflege",
"10. Sorglos-Paket (Betrieb & Pflege)"
];
standardTitles.forEach(std => {
const prefix = std.split('.')[0] + '.'; // e.g., "10."
// Find any key in the AI output that starts with this number
const matchingKey = Object.keys(normalized).find(k => k.trim().startsWith(prefix));
if (matchingKey && matchingKey !== std) {
normalized[std] = normalized[matchingKey];
// Keep the old key too just in case, but prioritize the standard one
}
});
finalState.positionDescriptions = normalized;
}
@@ -639,6 +788,69 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)}
}
}
// Final Post-Reflection Budget Sync (Hard Pruning if still over)
if (budget) {
const targetValue = parseInt(budget.replace(/[^0-9]/g, ''));
if (!isNaN(targetValue)) {
console.log(`⚖️ Final Budget Audit(${targetValue} € target)...`);
let currentTotals = calculateTotals(finalState, PRICING);
// Step-by-step pruning if too expensive
if (currentTotals.totalPrice > targetValue) {
console.log(`⚠️ Budget exceeded(${currentTotals.totalPrice} €).Pruning scope to fit ${targetValue} €...`);
// 1. Remove optional "other" stuff
finalState.otherFeatures = [];
finalState.otherFunctions = [];
finalState.otherTech = [];
// 2. Remove non-critical functions
const funcPriority = ['search', 'filter', 'calendar', 'multilang'];
for (const f of funcPriority) {
if (currentTotals.totalPrice <= targetValue) break;
if (finalState.functions.includes(f)) {
finalState.functions = finalState.functions.filter((x: string) => x !== f);
currentTotals = calculateTotals(finalState, PRICING);
}
}
// 3. Remove least critical features if still over
const featurePriority = ['events', 'blog_news', 'products'];
for (const p of featurePriority) {
if (currentTotals.totalPrice <= targetValue) break;
if (finalState.features.includes(p)) {
finalState.features = finalState.features.filter((f: string) => f !== p);
currentTotals = calculateTotals(finalState, PRICING);
}
}
// 4. Reduce page count (Selected Pages AND Sitemap)
while (currentTotals.totalPrice > targetValue && (finalState.selectedPages.length > 4 || currentTotals.totalPagesCount > 5)) {
if (finalState.selectedPages.length > 4) {
finalState.selectedPages.pop();
}
// Prune Sitemap to match
if (finalState.sitemap && Array.isArray(finalState.sitemap)) {
const lastCat = finalState.sitemap[finalState.sitemap.length - 1];
if (lastCat && lastCat.pages && lastCat.pages.length > 0) {
lastCat.pages.pop();
if (lastCat.pages.length === 0) finalState.sitemap.pop();
}
}
currentTotals = calculateTotals(finalState, PRICING);
}
// 5. Final fallback: Remove second feature if still over
if (currentTotals.totalPrice > targetValue && finalState.features.length > 1) {
finalState.features.pop();
currentTotals = calculateTotals(finalState, PRICING);
}
}
console.log(`✅ Final budget audit complete: ${currentTotals.totalPrice}`);
}
}
return { state: finalState, usage };
}