From 75bd0c29f38314be6672c358f08e5a52d9d6faec Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Fri, 24 Apr 2026 08:53:52 +0200 Subject: [PATCH] fix(pitch-deck): eliminate SYSTEM_PROMPT placeholder leak and fix liquidity tax ordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C3: Split SYSTEM_PROMPT into PART1/PART2/PART3 constants; Kernbotschaft #9 and VERSIONS-ISOLATION now concatenated directly at runtime instead of .replace() — a whitespace mismatch can no longer cause placeholder text to leak verbatim to the LLM. I2: Add second liquidity-chain pass (sumAus→ÜBERSCHUSS→rolling balance) after tax rows (Gewerbesteuer/Körperschaftsteuer) are written to fp_liquiditaet, so first-run LIQUIDITÄT figures include tax outflows without requiring a second engine invocation. I6: Warn when loadFpLiquiditaetSummary finds no fp_liquiditaet rows for a named scenario, surfacing scenario-name mismatches that would otherwise silently return empty context. I8: Sanitize console.error calls in chat/route.ts (3 sites) and data/route.ts; cap LiteLLM error body to 200 chars, use (error as Error).message for stream/handler errors. Co-Authored-By: Claude Sonnet 4.6 --- pitch-deck/app/api/chat/route.ts | 34 +++++++++++++--------- pitch-deck/app/api/data/route.ts | 2 +- pitch-deck/lib/finanzplan/engine.ts | 44 +++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/pitch-deck/app/api/chat/route.ts b/pitch-deck/app/api/chat/route.ts index 43f11a1..43de3ef 100644 --- a/pitch-deck/app/api/chat/route.ts +++ b/pitch-deck/app/api/chat/route.ts @@ -35,7 +35,8 @@ const SLIDE_DISPLAY_NAMES: Record = { const slideCount = SLIDE_ORDER.length -const SYSTEM_PROMPT = `# Investor Agent — BreakPilot ComplAI +// Static prefix: Identität through Kernbotschaft #8 — #9 and VERSIONS-ISOLATION injected at runtime +const SYSTEM_PROMPT_PART1 = `# Investor Agent — BreakPilot ComplAI ## Identität Du bist der BreakPilot ComplAI Investor Relations Agent. Du beantwortest Fragen von @@ -56,8 +57,10 @@ Du hast Zugriff auf alle Unternehmensdaten und zitierst immer konkrete Zahlen. 5. EU-Infrastruktur: "BSI-zertifizierte Cloud in Deutschland oder Frankreich. 100% Datensouveränität. KEINE US-Anbieter. Isolierte Namespaces." 6. Zielgruppen: "Maschinen- und Anlagenbauer, Automobilindustrie, Zulieferer und alle produzierenden Unternehmen." 7. Geschäftsmodell: "SaaS, mitarbeiterbasiertes Pricing. Drei Tiers: Starter (3.600 EUR/Jahr), Professional (15.000-40.000 EUR/Jahr), Enterprise (ab 50.000 EUR/Jahr). Plus Beratung & Service (10.000-30.000 EUR/Monat). Kunden sparen mehr als sie zahlen — ROI ab Tag 1." -8. Team: "Lean-Team: 2 Gründer + 7 Mitarbeiter bis 2030 (9 Personen gesamt). Erste Einstellung: IT-Recht/Datenschutzjurist (50%). Dann: Security Engineer, Vertrieb, Backend, Kundenbetreuer, Marketing, DevOps. Jede Einstellung an konkreten Umsatzmeilenstein gekoppelt." -9. Finanzplan: [SIEHE DYNAMISCHE VERSIONSDATEN — wird zur Laufzeit gesetzt] +8. Team: "Lean-Team: 2 Gründer + 7 Mitarbeiter bis 2030 (9 Personen gesamt). Erste Einstellung: IT-Recht/Datenschutzjurist (50%). Dann: Security Engineer, Vertrieb, Backend, Kundenbetreuer, Marketing, DevOps. Jede Einstellung an konkreten Umsatzmeilenstein gekoppelt."` + +// Static middle: Kommunikationsstil — injected between #9 and VERSIONS-ISOLATION +const SYSTEM_PROMPT_PART2 = ` ## Kommunikationsstil - Antworte IMMER wie ein Mensch in einem persönlichen Gespräch — ausformulierte Sätze, natürlicher Redefluss @@ -66,10 +69,10 @@ Du hast Zugriff auf alle Unternehmensdaten und zitierst immer konkrete Zahlen. - Nutze Übergangssätze wie "Der Grund dafür ist...", "Das haben wir bewusst so entschieden, weil...", "Besonders wichtig ist dabei..." - Zahlen und Fakten natürlich in den Text einbetten, nicht als Liste aufreihen - 3-5 Absätze pro Antwort, jeder Absatz ein eigenständiger Gedanke -- Der Text muss sich gut anhören wenn er vorgelesen wird (TTS-optimiert) +- Der Text muss sich gut anhören wenn er vorgelesen wird (TTS-optimiert)` -## VERSIONS-ISOLATION (ABSOLUT KRITISCH) -[WIRD ZUR LAUFZEIT GESETZT — enthält die exakten Zahlen dieser Investor-Version] +// Static suffix: everything after VERSIONS-ISOLATION +const SYSTEM_PROMPT_PART3 = ` ## IP-Schutz-Layer (KRITISCH) NIEMALS offenbaren: Exakte Modellnamen, Frameworks, Code-Architektur, Datenbankschema, Sicherheitsdetails, Cloud-Provider. @@ -123,7 +126,10 @@ async function loadFpLiquiditaetSummary(scenarioName: string): Promise { ORDER BY l.sort_order`, [scenarioName] ) - if (rows.length === 0) return '' + if (rows.length === 0) { + if (scenarioName) console.warn('[chat] loadFpLiquiditaetSummary: no rows for scenario', JSON.stringify(scenarioName)) + return '' + } const years = [2026, 2027, 2028, 2029, 2030] const summary: Record> = {} @@ -333,9 +339,11 @@ export async function POST(request: NextRequest) { const dynamicFinanzplanKernbotschaft = `9. Finanzplan: "Gründung August 2026. Pre-Seed über ${fundingStr}. ${customersStr} und ${revM} Umsatz bis 2030. ${employeesStr}."` - let systemContent = SYSTEM_PROMPT - .replace('9. Finanzplan: [SIEHE DYNAMISCHE VERSIONSDATEN — wird zur Laufzeit gesetzt]', dynamicFinanzplanKernbotschaft) - .replace('## VERSIONS-ISOLATION (ABSOLUT KRITISCH)\n[WIRD ZUR LAUFZEIT GESETZT — enthält die exakten Zahlen dieser Investor-Version]', dynamicVersionIsolation) + let systemContent = SYSTEM_PROMPT_PART1 + + '\n' + dynamicFinanzplanKernbotschaft + + SYSTEM_PROMPT_PART2 + + '\n\n' + dynamicVersionIsolation + + SYSTEM_PROMPT_PART3 if (contextString) { systemContent += '\n' + contextString @@ -402,7 +410,7 @@ export async function POST(request: NextRequest) { if (!llmResponse.ok) { const errorText = await llmResponse.text() - console.error('LiteLLM error:', llmResponse.status, errorText) + console.error('LiteLLM error:', llmResponse.status, errorText.slice(0, 200)) return NextResponse.json( { error: `LLM nicht erreichbar (Status ${llmResponse.status}).` }, { status: 502 } @@ -461,7 +469,7 @@ export async function POST(request: NextRequest) { } } } catch (error) { - console.error('Stream read error:', error) + console.error('Stream read error:', (error as Error).message) } finally { controller.close() } @@ -476,7 +484,7 @@ export async function POST(request: NextRequest) { }, }) } catch (error) { - console.error('Investor agent chat error:', error) + console.error('Investor agent chat error:', (error as Error).message) return NextResponse.json( { error: 'Verbindung zum LLM fehlgeschlagen.' }, { status: 503 } diff --git a/pitch-deck/app/api/data/route.ts b/pitch-deck/app/api/data/route.ts index 58899d9..cffd15a 100644 --- a/pitch-deck/app/api/data/route.ts +++ b/pitch-deck/app/api/data/route.ts @@ -77,7 +77,7 @@ export async function GET() { client.release() } } catch (error) { - console.error('Database query error:', error) + console.error('Database query error:', (error as Error).message) // Return minimal stub in dev so the pitch renders without a DB connection if (process.env.NODE_ENV === 'development') { return NextResponse.json({ diff --git a/pitch-deck/lib/finanzplan/engine.ts b/pitch-deck/lib/finanzplan/engine.ts index e3299ae..b66188b 100644 --- a/pitch-deck/lib/finanzplan/engine.ts +++ b/pitch-deck/lib/finanzplan/engine.ts @@ -623,6 +623,50 @@ export async function computeFinanzplan(pool: Pool, scenarioId: string): Promise liqKSt.values = v } + // Second pass: tax rows (liqGewSt/liqKSt) were written after the rolling balance above. + // Recompute Summe AUSZAHLUNGEN → ÜBERSCHUSS chain → rolling balance so taxes are included + // even on the first engine run (when tax rows were previously zero). + if (sumAus) { + const s = emptyMonthly() + for (const row of liquid) { + if (row.row_type === 'auszahlung' && row.id !== sumAus.id) { + for (let m = 1; m <= MONTHS; m++) s[`m${m}`] += Math.round(row.values[`m${m}`] || 0) + } + } + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), sumAus.id]) + sumAus.values = s + } + if (uebVorInv && sumEin && sumAus) { + const s = emptyMonthly() + for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = Math.round((sumEin.values[`m${m}`] || 0) - (sumAus.values[`m${m}`] || 0)) + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), uebVorInv.id]) + uebVorInv.values = s + } + if (uebVorEnt && uebVorInv && liqInvest) { + const s = emptyMonthly() + for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = Math.round((uebVorInv.values[`m${m}`] || 0) - (liqInvest.values[`m${m}`] || 0)) + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), uebVorEnt.id]) + uebVorEnt.values = s + } + if (ueberschuss && uebVorEnt && entnahmen) { + const s = emptyMonthly() + for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = Math.round((uebVorEnt.values[`m${m}`] || 0) - (entnahmen.values[`m${m}`] || 0)) + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), ueberschuss.id]) + ueberschuss.values = s + } + if (kontostand && liquiditaet && ueberschuss) { + const ks = emptyMonthly() + const lq = emptyMonthly() + for (let m = 1; m <= MONTHS; m++) { + ks[`m${m}`] = m === 1 ? 0 : Math.round(lq[`m${m - 1}`]) + lq[`m${m}`] = Math.round(ks[`m${m}`] + (ueberschuss.values[`m${m}`] || 0)) + } + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(ks), kontostand.id]) + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(lq), liquiditaet.id]) + kontostand.values = ks + liquiditaet.values = lq + } + return { personalkosten: { total_brutto: totalBrutto, total_sozial: totalSozial, total: totalPersonal, positions: personal, headcount }, investitionen: { total_invest: totalInvest, total_afa: totalAfa, items: invest },