refactor: architecture compliance pass — DS tokens, hook boundary, god component split, AI hardening

- DS token migration: Anfragen.tsx + child components (AnfragenInquiryItem, AnfragenMessageBubble)
  fully migrated; DS_TEXT.brandDark added; scoreTheme.ts moved to src/lib/ with re-export proxy
- Hook boundary: Results.tsx no longer calls needService directly — routes through useNeeds()
  with optional refetchOnMount/gcTime overrides
- NewListing.tsx (440L) split into useNewListingForm hook + 8 section components under
  src/components/new-listing/; page shell reduced to 121 lines
- AI hardening: Zod .strict() on all schemas, AIProvenance extended with schemaVersion/
  fallbackReason/traceId/latencyMs, AITraceStore stats with p50/p90/p99 + failure breakdowns,
  MockAIService buildFollowUpQuestions with priority ordering + area-ambiguity detection,
  prompt templates updated (LIGHT_INDUSTRIAL, budget unit, ambiguity detection, decimal precision)
- Tests: all 154 passing; fixed test regression caused by OfferEmailResponseSchema body min(50)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 16:10:39 +02:00
parent e36c5bc979
commit e1f4beb898
44 changed files with 1610 additions and 1058 deletions
@@ -24,8 +24,12 @@ PRIORITÄTSREIHENFOLGE:
5. timing (recommended) — wichtig für Verfügbarkeitsabgleich
6. mustHaveCriteria (optional) — Pflichtmerkmale (Parkplätze, Laderampe etc.)
MEHRDEUTIGKEITSERKENNUNG — prüfe auch bekannte Felder auf Ambiguität:
- areaRange vorhanden, aber max/min-Verhältnis > 5: generiere eine Präzisierungsfrage (targetField: "areaRange", importance: "required") statt sie als vollständig zu behandeln
- areaRange vorhanden, aber min = 0 oder max = 0: generiere dieselbe Präzisierungsfrage
VERBOTE — NIEMALS:
- Fragen zu bereits bekannten Kriterien stellen
- Fragen zu bereits bekannten, eindeutigen Kriterien stellen
- Mehr als 3 Fragen ausgeben
- Fragen erfinden, die nicht einem der 6 definierten Zielfelder entsprechen
- Doppelfragen stellen
@@ -54,7 +58,7 @@ Ausgabe:
"importance": "required"
},
{
"questionText": "Was ist Ihr maximales Budget pro m² und Monat (CHF)?",
"questionText": "Was ist Ihr maximales Budget pro m² und Jahr (CHF)?",
"targetField": "budgetRange",
"reason": "Budget ist wichtig für die Filterung unpassender Objekte",
"suggestedAnswerOptions": [],
@@ -34,6 +34,7 @@ WEITERE VERBOTE:
- Fläche in m² schätzen, wenn kein konkreter Hinweis im Text steht (→ null setzen)
- Zeithorizont nennen, wenn er nicht aus dem Text ableitbar ist (→ null setzen)
- probability > 0.85 setzen ohne mehrere unabhängige, verlässliche Bestätigungen
- probability mit mehr als 2 Dezimalstellen angeben (z.B. 0.724 → 0.72, 0.6666 → 0.67)
AUSGABEFORMAT — antworte ausschliesslich als valides JSON (kein Markdown-Block, keine Erklärungen):
{
+11 -5
View File
@@ -20,10 +20,10 @@ VERBOTE — NIEMALS:
AUSGABEFORMAT — antworte ausschliesslich als valides JSON (kein Markdown-Block, keine Erklärungen):
{
"assetType": "OFFICE" | "RETAIL" | "LOGISTICS" | "PRODUCTION" | "GASTRO" | "MIXED" | "UNKNOWN" | null,
"areaRange": { "min": number, "max": number } | null,
"assetType": "OFFICE" | "RETAIL" | "LOGISTICS" | "PRODUCTION" | "LIGHT_INDUSTRIAL" | "GASTRO" | "MIXED" | "UNKNOWN" | null,
"areaRange": { "min": number (≥1), "max": number (≥1, ≥ min) } | null,
"preferredLocations": string[],
"budgetRange": { "maxPerSqm": number, "currency": "CHF" } | null,
"budgetRange": { "maxPerSqm": number (CHF/m²/Jahr), "currency": "CHF" } | null,
"timing": {
"earliestMoveIn": "YYYY-MM-DD" | null,
"latestMoveIn": "YYYY-MM-DD" | null,
@@ -34,14 +34,20 @@ AUSGABEFORMAT — antworte ausschliesslich als valides JSON (kein Markdown-Block
"assumptions": string[]
}
FELDREGELN:
- areaRange.min und areaRange.max müssen beide ≥ 1 sein — nie 0 setzen
- budgetRange.maxPerSqm ist CHF pro m² pro Jahr (Jahresmiete) — nicht Monatsmiete
- LIGHT_INDUSTRIAL: Leichtindustrielle Nutzung (Werkstatt, Atelier, kleine Produktion), klar abgegrenzt von LOGISTICS
- Maximale Einträge: preferredLocations max. 20, mustHaveCriteria max. 10
BEISPIEL:
Eingabe: "Wir suchen ein Büro für ca. 20 Personen in Zürich, Budget rund 50 CHF/m², Einzug ab März 2026"
Eingabe: "Wir suchen ein Büro für ca. 20 Personen in Zürich, Budget rund 600 CHF/m²/Jahr, Einzug ab März 2026"
Ausgabe:
{
"assetType": "OFFICE",
"areaRange": { "min": 200, "max": 400 },
"preferredLocations": ["Zürich"],
"budgetRange": { "maxPerSqm": 50, "currency": "CHF" },
"budgetRange": { "maxPerSqm": 600, "currency": "CHF" },
"timing": { "earliestMoveIn": "2026-03-01", "latestMoveIn": null, "flexibleTiming": false },
"mustHaveCriteria": [],
"missingFields": ["timing.latestMoveIn", "mustHaveCriteria"],