From 66c489a431e5e34866149d023dcd316fadd4ea7a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Dec 2025 16:09:21 +0000 Subject: [PATCH 1/4] Add comprehensive design and frontend implementation documentation New documentation files: - docs/DESIGN_SPECIFICATION.md: Complete UI/UX design guide - Color palette with risk-level colors - Typography scale and hierarchy - Component library specifications (badges, cards, alerts, tables) - Page layouts (dashboard, agents, alerts, reports) - Interaction patterns and animations - Responsive design breakpoints - Accessibility guidelines (A11Y) - Dark mode support - Icon set recommendations - docs/FRONTEND_IMPLEMENTATION.md: Technical implementation guide - Recommended tech stack (Next.js, TypeScript, Tailwind) - Project structure - TypeScript type definitions - Component implementations (React/TSX) - Layout components (sidebar, header) - State management with Zustand - TanStack Query hooks - WebSocket integration for real-time updates - API client setup - Tailwind configuration This documentation enables design and frontend agents to create a consistent, accessible, and performant dashboard UI. --- docs/DESIGN_SPECIFICATION.md | 1005 ++++++++++++++++++++++++++++ docs/FRONTEND_IMPLEMENTATION.md | 1087 +++++++++++++++++++++++++++++++ 2 files changed, 2092 insertions(+) create mode 100644 docs/DESIGN_SPECIFICATION.md create mode 100644 docs/FRONTEND_IMPLEMENTATION.md diff --git a/docs/DESIGN_SPECIFICATION.md b/docs/DESIGN_SPECIFICATION.md new file mode 100644 index 0000000..a76e412 --- /dev/null +++ b/docs/DESIGN_SPECIFICATION.md @@ -0,0 +1,1005 @@ +# Step2Job Cloud Agents - Design & UI/UX Spezifikation + +## Komplette Anleitung für Design-Agenten + +--- + +## 1. PROJEKT-ÜBERBLICK + +### 1.1 Produktvision +**Step2Job Cloud Agents** ist ein Supervisor-Dashboard zur Überwachung und Qualitätssicherung von KI-Agenten. Das System analysiert Gespräche in Echtzeit und warnt Supervisoren bei kritischen Vorfällen. + +### 1.2 Zielgruppen + +| Persona | Rolle | Bedürfnisse | +|---------|-------|-------------| +| **Supervisor Sarah** | Team-Leiterin (5-10 Agenten) | Schneller Überblick, sofortige Alerts, Batch-Aktionen | +| **Manager Michael** | Abteilungsleiter | Reports, Trends, KPIs, Export-Funktionen | +| **Admin Anna** | System-Administratorin | Konfiguration, Benutzerverwaltung, Logs | +| **Agent Alex** | KI-Agent-Operator | Eigene Performance sehen, Feedback verstehen | + +### 1.3 Kernfunktionen +1. **Live-Dashboard** - Echtzeit-Überwachung aller Agenten +2. **Alert-Center** - Kritische Vorfälle sofort erkennen +3. **Agent-Details** - Einzelne Agenten analysieren +4. **Reports** - Historische Daten und Trends +5. **Konfiguration** - Keywords und Schwellwerte anpassen + +--- + +## 2. DESIGN-SYSTEM + +### 2.1 Farbpalette + +#### Primärfarben +```css +:root { + /* Brand Colors */ + --primary-50: #EEF2FF; + --primary-100: #E0E7FF; + --primary-200: #C7D2FE; + --primary-300: #A5B4FC; + --primary-400: #818CF8; + --primary-500: #6366F1; /* Hauptfarbe */ + --primary-600: #4F46E5; + --primary-700: #4338CA; + --primary-800: #3730A3; + --primary-900: #312E81; +} +``` + +#### Risk-Level Farben (KRITISCH - konsistent verwenden!) +```css +:root { + /* Risk Colors - Semantisch */ + --risk-low: #10B981; /* Grün - Erfolg */ + --risk-low-bg: #D1FAE5; + --risk-low-border: #6EE7B7; + + --risk-medium: #F59E0B; /* Gelb/Orange - Warnung */ + --risk-medium-bg: #FEF3C7; + --risk-medium-border: #FCD34D; + + --risk-high: #F97316; /* Orange - Gefahr */ + --risk-high-bg: #FFEDD5; + --risk-high-border: #FDBA74; + + --risk-critical: #EF4444; /* Rot - Kritisch */ + --risk-critical-bg: #FEE2E2; + --risk-critical-border: #FCA5A5; +} +``` + +#### Neutrale Farben +```css +:root { + --gray-50: #F9FAFB; + --gray-100: #F3F4F6; + --gray-200: #E5E7EB; + --gray-300: #D1D5DB; + --gray-400: #9CA3AF; + --gray-500: #6B7280; + --gray-600: #4B5563; + --gray-700: #374151; + --gray-800: #1F2937; + --gray-900: #111827; +} +``` + +### 2.2 Typografie + +```css +:root { + /* Font Familie */ + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', monospace; + + /* Font Sizes */ + --text-xs: 0.75rem; /* 12px */ + --text-sm: 0.875rem; /* 14px */ + --text-base: 1rem; /* 16px */ + --text-lg: 1.125rem; /* 18px */ + --text-xl: 1.25rem; /* 20px */ + --text-2xl: 1.5rem; /* 24px */ + --text-3xl: 1.875rem; /* 30px */ + --text-4xl: 2.25rem; /* 36px */ + + /* Font Weights */ + --font-normal: 400; + --font-medium: 500; + --font-semibold: 600; + --font-bold: 700; + + /* Line Heights */ + --leading-tight: 1.25; + --leading-normal: 1.5; + --leading-relaxed: 1.75; +} +``` + +#### Typografie-Hierarchie + +| Element | Größe | Gewicht | Verwendung | +|---------|-------|---------|------------| +| H1 | 36px | Bold | Seitentitel | +| H2 | 24px | Semibold | Sektionsüberschriften | +| H3 | 20px | Semibold | Card-Titel | +| H4 | 18px | Medium | Subsektionen | +| Body | 16px | Normal | Fließtext | +| Small | 14px | Normal | Sekundärtext | +| Caption | 12px | Normal | Labels, Timestamps | + +### 2.3 Abstände & Grid + +```css +:root { + /* Spacing Scale (8px Basis) */ + --space-1: 0.25rem; /* 4px */ + --space-2: 0.5rem; /* 8px */ + --space-3: 0.75rem; /* 12px */ + --space-4: 1rem; /* 16px */ + --space-5: 1.25rem; /* 20px */ + --space-6: 1.5rem; /* 24px */ + --space-8: 2rem; /* 32px */ + --space-10: 2.5rem; /* 40px */ + --space-12: 3rem; /* 48px */ + --space-16: 4rem; /* 64px */ + + /* Border Radius */ + --radius-sm: 0.25rem; /* 4px */ + --radius-md: 0.375rem; /* 6px */ + --radius-lg: 0.5rem; /* 8px */ + --radius-xl: 0.75rem; /* 12px */ + --radius-2xl: 1rem; /* 16px */ + --radius-full: 9999px; + + /* Shadows */ + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1); + --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1); +} +``` + +### 2.4 Grid-System + +``` +Desktop (≥1280px): 12-Spalten Grid, 24px Gutter +Tablet (768-1279px): 8-Spalten Grid, 20px Gutter +Mobile (<768px): 4-Spalten Grid, 16px Gutter + +Container Max-Width: 1440px +Sidebar-Breite: 256px (collapsed: 64px) +``` + +--- + +## 3. KOMPONENTEN-BIBLIOTHEK + +### 3.1 Risk-Level Badge + +``` +┌─────────────────┐ +│ ● LOW │ Grün, gefüllter Kreis +└─────────────────┘ + +┌─────────────────┐ +│ ▲ MEDIUM │ Gelb, Dreieck-Warnung +└─────────────────┘ + +┌─────────────────┐ +│ ◆ HIGH │ Orange, Raute +└─────────────────┘ + +┌─────────────────┐ +│ ✖ CRITICAL │ Rot, X-Symbol, pulsierend +└─────────────────┘ +``` + +**Spezifikation:** +- Padding: 4px 12px +- Border-Radius: 9999px (Pill) +- Font: 12px, Semibold, Uppercase +- Icon links, 4px Abstand zum Text + +### 3.2 Agent-Card + +``` +┌──────────────────────────────────────────────────────┐ +│ ┌────┐ │ +│ │ 🤖 │ AGENT_001 ● Online │ +│ └────┘ Max Mustermann │ +│ │ +│ ┌─────────────┬─────────────┬─────────────┐ │ +│ │ Calls Today │ Avg. Risk │ Stop Rate │ │ +│ │ 24 │ 0.8 │ 92% │ │ +│ └─────────────┴─────────────┴─────────────┘ │ +│ │ +│ Risk Distribution: │ +│ ████████████████░░░░ LOW (80%) │ +│ ████░░░░░░░░░░░░░░░░ MEDIUM (15%) │ +│ █░░░░░░░░░░░░░░░░░░░ HIGH (5%) │ +│ │ +│ [View Details] [Pause Agent] │ +└──────────────────────────────────────────────────────┘ +``` + +**Spezifikation:** +- Breite: 100% (Grid-responsive) +- Min-Breite: 320px +- Padding: 24px +- Background: White +- Border: 1px solid gray-200 +- Border-Radius: 12px +- Shadow: shadow-md +- Hover: shadow-lg, border-primary-200 + +### 3.3 Alert-Item + +``` +┌──────────────────────────────────────────────────────────────┐ +│ ⚠️ CRITICAL vor 2 Min │ +├──────────────────────────────────────────────────────────────┤ +│ │ +│ Agent AGENT_003 - Maria Weber │ +│ │ +│ Verstöße: │ +│ • Price mentioned without fact │ +│ • No STOP on legal question │ +│ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ "Das kostet 500€ und ist gesetzlich völlig in │ │ +│ │ Ordnung..." │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +│ [Transcript ansehen] [Agent pausieren] [Als gelesen] │ +└──────────────────────────────────────────────────────────────┘ +``` + +**Spezifikation:** +- Border-Left: 4px solid (Risk-Farbe) +- Background: Risk-Farbe-bg (10% Opacity) +- Padding: 16px 20px +- Ungelesen: Font-weight bold, Indikator-Dot + +### 3.4 Stat-Card (KPI) + +``` +┌─────────────────────────┐ +│ ↗️ +12% │ +│ │ +│ 247 │ +│ Calls Today │ +│ │ +│ ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁ │ +└─────────────────────────┘ +``` + +**Spezifikation:** +- Min-Breite: 160px +- Padding: 20px +- Trend-Indikator: Grün (positiv), Rot (negativ) +- Sparkline: Optional, 32px Höhe + +### 3.5 Button-Varianten + +``` +Primary: [████████████████] Gefüllt, Primary-600 +Secondary: [────────────────] Outlined, Primary-600 Border +Ghost: [ Button Text ] Nur Text, Hover: gray-100 bg +Danger: [████████████████] Gefüllt, Red-600 +Success: [████████████████] Gefüllt, Green-600 +``` + +**Größen:** +- Small: 32px Höhe, 12px Font, 12px Padding +- Medium: 40px Höhe, 14px Font, 16px Padding +- Large: 48px Höhe, 16px Font, 20px Padding + +### 3.6 Input-Felder + +``` +Label +┌──────────────────────────────────┐ +│ 🔍 Placeholder text... │ +└──────────────────────────────────┘ +Helper text oder Fehler + +States: +- Default: gray-300 border +- Focus: primary-500 border, ring +- Error: red-500 border +- Disabled: gray-100 bg, gray-400 text +``` + +### 3.7 Data Table + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ☐ Agent ID │ Contact │ Time │ Risk │ Actions │ +├─────────────────────────────────────────────────────────────────┤ +│ ☐ AGENT_001 │ M. Mustermann│ 10:30:00 │ ● LOW │ [⋮] │ +│ ☑ AGENT_002 │ A. Schmidt │ 10:32:15 │ ▲ MED │ [⋮] │ +│ ☐ AGENT_003 │ T. Müller │ 10:35:42 │ ✖ CRIT │ [⋮] │ +├─────────────────────────────────────────────────────────────────┤ +│ Showing 1-10 of 247 │ [<] [1] [2] [3] ... [25] [>] │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Spezifikation:** +- Header: gray-50 bg, font-semibold +- Rows: Alternating gray-50/white (optional) +- Hover: primary-50 bg +- Selected: primary-100 bg +- Border: gray-200 +- Sortierbare Spalten: Pfeil-Icon + +--- + +## 4. SEITEN-LAYOUTS + +### 4.1 Haupt-Layout + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ ┌────────┐ 🔔 3 👤 Sarah │ +│ │ LOGO │ Step2Job Cloud Agents Notifications User │ +├──────────┴───────────────────────────────────────────────────────────┤ +│ │ │ │ +│ │ 📊 │ ┌─────────────────────────────────────────────────────┐│ +│ │ Dash │ │ ││ +│ │ │ │ MAIN CONTENT AREA ││ +│ │ 🤖 │ │ ││ +│ │ Agents │ │ ││ +│ │ │ │ ││ +│ │ 🔔 │ │ ││ +│ │ Alerts │ │ ││ +│ │ │ │ ││ +│ │ 📈 │ │ ││ +│ │ Reports│ │ ││ +│ │ │ │ ││ +│ │ ⚙️ │ │ ││ +│ │ Config │ └─────────────────────────────────────────────────────┘│ +│ │ │ │ +└──┴────────┴──────────────────────────────────────────────────────────┘ +``` + +### 4.2 Dashboard-Seite + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ Dashboard [Export] [Refresh 🔄] │ +│ Last updated: 23.12.2025, 15:30:00 │ +├──────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Active │ │ Calls │ │ Avg Risk │ │ Alerts │ │ +│ │ Agents │ │ Today │ │ Score │ │ Today │ │ +│ │ 5 │ │ 247 │ │ 0.6 │ │ 3 │ │ +│ │ ↗️ +2 │ │ ↗️ +12% │ │ ↘️ -0.2 │ │ ↗️ +1 │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +│ ┌─────────────────────────────┐ ┌────────────────────────────────┐ │ +│ │ Risk Distribution │ │ Recent Alerts │ │ +│ │ │ │ │ │ +│ │ ████████████ LOW (75%) │ │ ⚠️ AGENT_003 - CRITICAL │ │ +│ │ ████░░░░░░░░ MED (15%) │ │ Price without fact │ │ +│ │ ██░░░░░░░░░░ HIGH (8%) │ │ 2 minutes ago │ │ +│ │ █░░░░░░░░░░░ CRIT (2%) │ │ ───────────────────────────── │ │ +│ │ │ │ ▲ AGENT_002 - MEDIUM │ │ +│ │ [View Details] │ │ Timeline assumption │ │ +│ └─────────────────────────────┘ │ 15 minutes ago │ │ +│ │ ───────────────────────────── │ │ +│ ┌─────────────────────────────┐ │ ● AGENT_001 - LOW │ │ +│ │ Agents Overview │ │ Resolved │ │ +│ │ │ │ 1 hour ago │ │ +│ │ Agent Status Risk │ │ │ │ +│ │ ───────────────────────── │ │ [View All Alerts →] │ │ +│ │ AGENT_001 ● Online LOW │ └────────────────────────────────┘ │ +│ │ AGENT_002 ● Online MED │ │ +│ │ AGENT_003 ⚠ Review HIGH │ │ +│ │ AGENT_004 ● Online LOW │ │ +│ │ │ │ +│ │ [View All Agents →] │ │ +│ └─────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────────┐ │ +│ │ Performance Trend (Last 7 Days) │ │ +│ │ │ │ +│ │ 1.5 ┤ │ │ +│ │ │ ╭─╮ │ │ +│ │ 1.0 ┤ ╭───╯ ╰──╮ │ │ +│ │ │ ╱ ╰───────────╮ │ │ +│ │ 0.5 ┤──╯ ╰── │ │ +│ │ │ │ │ +│ │ 0.0 ┼─────────────────────────────────── │ │ +│ │ Mo Di Mi Do Fr Sa So │ │ +│ │ │ │ +│ │ ── Avg Risk ── Critical Incidents ── Stop Rate │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +### 4.3 Agent-Detail-Seite + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ ← Back to Agents │ +│ │ +│ ┌────────────────────────────────────────────────────────────────┐ │ +│ │ ┌──────┐ │ │ +│ │ │ 🤖 │ AGENT_001 │ │ +│ │ │ │ Supervisor: Sarah Schmidt │ │ +│ │ └──────┘ Status: ● Online since 08:00 │ │ +│ │ │ │ +│ │ [Pause Agent] [View Logs] [Edit Config] [⋮ More] │ │ +│ └────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ Total Calls │ │ Average Risk │ │ Stop Rate │ │ +│ │ 156 │ │ 0.4 │ │ 95% │ │ +│ │ this week │ │ ● LOW │ │ excellent │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────────┐ │ +│ │ [Recent Calls] [Violations] [Statistics] [Settings] │ │ +│ ├──────────────────────────────────────────────────────────────┤ │ +│ │ │ │ +│ │ Today's Calls │ │ +│ │ ───────────────────────────────────────────────────────── │ │ +│ │ │ │ +│ │ 10:35:42 │ Thomas Müller │ ● LOW │ [View Transcript] │ │ +│ │ 10:32:15 │ Anna Schmidt │ ▲ MED │ [View Transcript] │ │ +│ │ 10:30:00 │ Max Mustermann │ ● LOW │ [View Transcript] │ │ +│ │ 10:25:33 │ Lisa Braun │ ● LOW │ [View Transcript] │ │ +│ │ 10:20:17 │ Peter Hoffmann │ ● LOW │ [View Transcript] │ │ +│ │ │ │ +│ │ [Load More...] │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +### 4.4 Alert-Center + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ Alert Center [Mark All Read] [Settings ⚙️]│ +│ │ +│ ┌────────────────────────────────────────────────────────────────┐ │ +│ │ 🔍 Search alerts... │ Risk: [All ▾] │ Date: [Today ▾] │ │ +│ └────────────────────────────────────────────────────────────────┘ │ +│ │ +│ Unread (3) │ +│ ───────────────────────────────────────────────────────────────── │ +│ │ +│ ┌────────────────────────────────────────────────────────────────┐ │ +│ │ ⚠️ CRITICAL vor 2 Min │ │ +│ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ +│ │ AGENT_003 - Maria Weber │ │ +│ │ │ │ +│ │ • Price mentioned without fact reference │ │ +│ │ • No STOP on legal question │ │ +│ │ │ │ +│ │ "Das kostet 500€ und ist gesetzlich völlig in Ordnung..." │ │ +│ │ │ │ +│ │ [View Full] [Pause Agent] [Dismiss] │ │ +│ └────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌────────────────────────────────────────────────────────────────┐ │ +│ │ ▲ MEDIUM vor 15 Min │ │ +│ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ +│ │ AGENT_002 - Anna Schmidt │ │ +│ │ │ │ +│ │ • Timeline assumption without PLACEHOLDER │ │ +│ │ │ │ +│ │ [View Full] [Dismiss] │ │ +│ └────────────────────────────────────────────────────────────────┘ │ +│ │ +│ Read (47) │ +│ ───────────────────────────────────────────────────────────────── │ +│ │ +│ [Show older alerts...] │ +│ │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +### 4.5 Reports-Seite + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ Reports │ +│ │ +│ ┌────────────────────────────────────────────────────────────────┐ │ +│ │ Period: [Last 7 Days ▾] │ Agents: [All ▾] │ [Generate Report]│ │ +│ └────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌────────────────────────────────────────────────────────────────┐ │ +│ │ Summary Statistics │ │ +│ │ │ │ +│ │ Total Calls Price Claims Legal Claims Stop Rate │ │ +│ │ 1,247 89 34 94% │ │ +│ │ ↗️ +15% ↘️ -8% ↘️ -12% ↗️ +3% │ │ +│ └────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────┐ ┌────────────────────────────────┐ │ +│ │ Risk Trend │ │ Top Issues │ │ +│ │ │ │ │ │ +│ │ ▲ │ │ 1. Price without fact (42%) │ │ +│ │ │ ╭─╮ │ │ 2. Legal assumption (28%) │ │ +│ │ │╭──╯ ╰╮ │ │ 3. No STOP trigger (18%) │ │ +│ │ ╰╯ ╰────── │ │ 4. Timeline guess (12%) │ │ +│ │ Mo Di Mi Do Fr Sa So │ │ │ │ +│ └─────────────────────────────┘ └────────────────────────────────┘ │ +│ │ +│ ┌────────────────────────────────────────────────────────────────┐ │ +│ │ Agent Performance Ranking │ │ +│ │ │ │ +│ │ Rank │ Agent │ Calls │ Avg Risk │ Stop Rate │ Trend │ │ +│ │ ─────┼────────────┼───────┼──────────┼───────────┼────────── │ │ +│ │ 1 │ AGENT_001 │ 156 │ 0.3 │ 98% │ ↗️ +5% │ │ +│ │ 2 │ AGENT_004 │ 142 │ 0.4 │ 96% │ → 0% │ │ +│ │ 3 │ AGENT_002 │ 138 │ 0.6 │ 92% │ ↘️ -3% │ │ +│ │ 4 │ AGENT_003 │ 124 │ 1.2 │ 78% │ ↘️ -8% │ │ +│ │ │ │ +│ └────────────────────────────────────────────────────────────────┘ │ +│ │ +│ Export: [📄 PDF] [📊 Excel] [📋 CSV] [🖼️ PNG] │ +│ │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 5. INTERAKTIONS-PATTERNS + +### 5.1 Echtzeit-Updates + +``` +WebSocket Events: +├── agent.status.changed → Agent-Card Status aktualisieren +├── alert.new → Toast + Badge-Counter + Alert-Liste +├── score.calculated → Dashboard-Statistiken aktualisieren +└── agent.paused → Agent-Card visuell markieren + +Toast-Notification (Critical Alert): +┌────────────────────────────────────────┐ +│ ⚠️ CRITICAL ALERT ✕ │ +│ AGENT_003 needs immediate review │ +│ [View Now] │ +└────────────────────────────────────────┘ +Position: Top-Right, Auto-dismiss: 10s (nicht bei Critical) +``` + +### 5.2 Loading States + +``` +Skeleton Loading: +┌──────────────────────────────────────┐ +│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ +│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ +│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ +└──────────────────────────────────────┘ + +Spinner (Inline): +[⟳ Loading...] + +Progress Bar (Batch-Operations): +████████████░░░░░░░░░ 60% - Processing 150 of 247 logs +``` + +### 5.3 Empty States + +``` +┌──────────────────────────────────────┐ +│ │ +│ 📭 │ +│ │ +│ No alerts found │ +│ │ +│ Great job! All agents are │ +│ performing within parameters. │ +│ │ +│ [View Agent Status] │ +│ │ +└──────────────────────────────────────┘ +``` + +### 5.4 Error States + +``` +┌──────────────────────────────────────┐ +│ ❌ Connection Lost │ +│ │ +│ Unable to connect to the server. │ +│ Data may be outdated. │ +│ │ +│ [Retry] [Work Offline] │ +│ │ +│ Last updated: 5 minutes ago │ +└──────────────────────────────────────┘ +``` + +### 5.5 Confirmation Dialogs + +``` +┌──────────────────────────────────────────────┐ +│ ⚠️ Pause Agent? │ +├──────────────────────────────────────────────┤ +│ │ +│ Are you sure you want to pause AGENT_003? │ +│ │ +│ This agent will stop processing new calls │ +│ until manually resumed. │ +│ │ +│ Reason (optional): │ +│ ┌────────────────────────────────────────┐ │ +│ │ Performance review required │ │ +│ └────────────────────────────────────────┘ │ +│ │ +│ [Cancel] [Pause Agent] │ +│ │ +└──────────────────────────────────────────────┘ +``` + +--- + +## 6. RESPONSIVE DESIGN + +### 6.1 Breakpoints + +```css +/* Mobile First Approach */ +--breakpoint-sm: 640px; /* Small tablets */ +--breakpoint-md: 768px; /* Tablets */ +--breakpoint-lg: 1024px; /* Small laptops */ +--breakpoint-xl: 1280px; /* Desktops */ +--breakpoint-2xl: 1536px; /* Large screens */ +``` + +### 6.2 Mobile Layout (< 768px) + +``` +┌────────────────────────┐ +│ ☰ Step2Job 🔔 👤 │ +├────────────────────────┤ +│ │ +│ ┌──────────────────┐ │ +│ │ Active: 5 │ │ +│ │ Alerts: 3 ⚠️ │ │ +│ └──────────────────┘ │ +│ │ +│ Recent Alerts │ +│ ──────────────────── │ +│ ┌──────────────────┐ │ +│ │ ⚠️ CRITICAL │ │ +│ │ AGENT_003 │ │ +│ │ [View →] │ │ +│ └──────────────────┘ │ +│ │ +│ ┌──────────────────┐ │ +│ │ ▲ MEDIUM │ │ +│ │ AGENT_002 │ │ +│ │ [View →] │ │ +│ └──────────────────┘ │ +│ │ +├────────────────────────┤ +│ 📊 🤖 🔔 📈 ⚙️ │ +└────────────────────────┘ +``` + +**Mobile-Anpassungen:** +- Bottom-Navigation statt Sidebar +- Vollbreite Cards (stacked) +- Swipe-Gesten für Tabs +- Collapsed Stats (expandable) +- Touch-optimierte Buttons (min 44x44px) + +### 6.3 Tablet Layout (768px - 1024px) + +``` +┌───────────────────────────────────────────┐ +│ LOGO Step2Job Cloud Agents 🔔 👤 │ +├────────┬──────────────────────────────────┤ +│ │ │ +│ 📊 │ ┌─────────┐ ┌─────────┐ │ +│ │ │ Active │ │ Alerts │ │ +│ 🤖 │ │ 5 │ │ 3 │ │ +│ │ └─────────┘ └─────────┘ │ +│ 🔔 │ │ +│ │ ┌────────────────────────────┐ │ +│ 📈 │ │ Recent Alerts │ │ +│ │ │ ... │ │ +│ ⚙️ │ └────────────────────────────┘ │ +│ │ │ +└────────┴──────────────────────────────────┘ +``` + +--- + +## 7. ACCESSIBILITY (A11Y) + +### 7.1 Farb-Kontrast +- Alle Text/Hintergrund-Kombinationen: min. 4.5:1 (WCAG AA) +- Risk-Level nicht nur durch Farbe, auch durch Icons/Text + +### 7.2 Keyboard Navigation +``` +Tab-Reihenfolge: +1. Skip to main content Link +2. Navigation Items +3. Main Content (logische Reihenfolge) +4. Footer Actions + +Shortcuts: +- `?` → Keyboard shortcuts anzeigen +- `g d` → Go to Dashboard +- `g a` → Go to Agents +- `g l` → Go to Alerts +- `j/k` → Nächster/Vorheriger Item in Liste +- `Enter` → Item öffnen +- `Esc` → Modal/Dropdown schließen +``` + +### 7.3 Screen Reader Support +```html + + + + CRITICAL + + + + + + +
+ New critical alert: Agent 003 requires review +
+``` + +### 7.4 Focus Indicators +```css +:focus-visible { + outline: 2px solid var(--primary-500); + outline-offset: 2px; +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + :focus-visible { + outline: 3px solid currentColor; + } +} +``` + +--- + +## 8. ANIMATIONEN & TRANSITIONS + +### 8.1 Timing Functions + +```css +:root { + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in: cubic-bezier(0.4, 0, 1, 1); + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + + --duration-fast: 150ms; + --duration-normal: 200ms; + --duration-slow: 300ms; +} +``` + +### 8.2 Standard-Transitions + +```css +/* Buttons, Links */ +.interactive { + transition: all var(--duration-fast) var(--ease-out); +} + +/* Cards, Panels */ +.card { + transition: box-shadow var(--duration-normal) var(--ease-out), + transform var(--duration-normal) var(--ease-out); +} + +/* Modals, Dropdowns */ +.overlay { + transition: opacity var(--duration-normal) var(--ease-out); +} +``` + +### 8.3 Spezielle Animationen + +```css +/* Critical Alert Pulse */ +@keyframes pulse-critical { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.alert-critical { + animation: pulse-critical 2s ease-in-out infinite; +} + +/* New Item Slide-In */ +@keyframes slide-in { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.new-item { + animation: slide-in var(--duration-normal) var(--ease-out); +} + +/* Skeleton Loading Shimmer */ +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +.skeleton { + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} +``` + +--- + +## 9. DARK MODE + +### 9.1 Farbpalette Dark + +```css +[data-theme="dark"] { + --bg-primary: #0F172A; + --bg-secondary: #1E293B; + --bg-tertiary: #334155; + + --text-primary: #F8FAFC; + --text-secondary: #CBD5E1; + --text-muted: #64748B; + + --border-color: #334155; + + /* Risk colors bleiben gleich, aber mit angepasster Opacity */ + --risk-low-bg: rgba(16, 185, 129, 0.15); + --risk-medium-bg: rgba(245, 158, 11, 0.15); + --risk-high-bg: rgba(249, 115, 22, 0.15); + --risk-critical-bg: rgba(239, 68, 68, 0.15); +} +``` + +### 9.2 System Preference Detection + +```css +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + /* Dark mode variables */ + } +} +``` + +--- + +## 10. ICON-SET + +### 10.1 Empfohlene Icons (Lucide/Heroicons) + +| Funktion | Icon | Code | +|----------|------|------| +| Dashboard | LayoutDashboard | `` | +| Agents | Bot | `` | +| Alerts | Bell | `` | +| Reports | BarChart3 | `` | +| Settings | Settings | `` | +| Risk LOW | CheckCircle | `` | +| Risk MEDIUM | AlertTriangle | `` | +| Risk HIGH | AlertOctagon | `` | +| Risk CRITICAL | XCircle | `` | +| Refresh | RefreshCw | `` | +| Export | Download | `` | +| Filter | Filter | `` | +| Search | Search | `` | +| User | User | `` | +| Menu | Menu | `` | +| Close | X | `` | +| Expand | ChevronDown | `` | +| External | ExternalLink | `` | + +--- + +## 11. IMPLEMENTIERUNGS-CHECKLISTE + +### Phase 1: Foundation +- [ ] Design System in Figma/Sketch erstellen +- [ ] Farbpalette definieren und testen +- [ ] Typografie-Scale implementieren +- [ ] Grid-System aufsetzen +- [ ] Icon-Set auswählen und integrieren + +### Phase 2: Komponenten +- [ ] Button-Varianten +- [ ] Input-Felder +- [ ] Risk-Level Badges +- [ ] Agent-Cards +- [ ] Alert-Items +- [ ] Stat-Cards +- [ ] Data Tables +- [ ] Modals/Dialogs +- [ ] Toast Notifications +- [ ] Loading States +- [ ] Empty States + +### Phase 3: Layouts +- [ ] Haupt-Layout mit Sidebar +- [ ] Dashboard-Seite +- [ ] Agent-Liste +- [ ] Agent-Detail +- [ ] Alert-Center +- [ ] Reports-Seite +- [ ] Settings-Seite + +### Phase 4: Interaktionen +- [ ] Hover/Focus States +- [ ] Transitions/Animations +- [ ] Keyboard Navigation +- [ ] Touch-Gesten (Mobile) +- [ ] Real-time Updates (WebSocket) + +### Phase 5: Polish +- [ ] Dark Mode +- [ ] Responsive Testing +- [ ] Accessibility Audit +- [ ] Performance Optimization +- [ ] Browser Testing + +--- + +## 12. DESIGN-ASSETS + +### 12.1 Benötigte Dateien + +``` +/design +├── /figma +│ ├── step2job-design-system.fig +│ ├── step2job-components.fig +│ └── step2job-screens.fig +├── /exports +│ ├── /icons (SVG) +│ ├── /images (PNG, WebP) +│ └── /logos (SVG, PNG) +├── /tokens +│ └── design-tokens.json +└── /docs + └── component-specs.pdf +``` + +### 12.2 Handoff-Format + +Für Developer-Handoff empfohlen: +- **Figma** mit Dev Mode aktiviert +- **Design Tokens** als JSON für CSS-Variables +- **Storybook** für Komponenten-Dokumentation + +--- + +**Dokument-Version:** 1.0 +**Erstellt:** 23.12.2025 +**Autor:** Design Agent +**Status:** Draft - Zur Review diff --git a/docs/FRONTEND_IMPLEMENTATION.md b/docs/FRONTEND_IMPLEMENTATION.md new file mode 100644 index 0000000..935bd8d --- /dev/null +++ b/docs/FRONTEND_IMPLEMENTATION.md @@ -0,0 +1,1087 @@ +# Frontend-Implementierung - Technische Anleitung + +## Empfohlener Tech-Stack + +### Framework & Build +```json +{ + "framework": "Next.js 14+ (App Router)", + "language": "TypeScript 5+", + "styling": "Tailwind CSS 3.4+", + "components": "shadcn/ui", + "state": "Zustand oder TanStack Query", + "charts": "Recharts oder Tremor", + "icons": "Lucide React", + "forms": "React Hook Form + Zod" +} +``` + +--- + +## 1. PROJEKT-SETUP + +```bash +# Projekt erstellen +npx create-next-app@latest step2job-dashboard --typescript --tailwind --eslint --app + +# Abhängigkeiten installieren +npm install @tanstack/react-query zustand recharts lucide-react +npm install react-hook-form zod @hookform/resolvers +npm install date-fns clsx tailwind-merge class-variance-authority + +# shadcn/ui initialisieren +npx shadcn-ui@latest init + +# Komponenten hinzufügen +npx shadcn-ui@latest add button card badge table dialog +npx shadcn-ui@latest add dropdown-menu tabs tooltip avatar +npx shadcn-ui@latest add input select textarea checkbox +npx shadcn-ui@latest add alert toast sonner skeleton +``` + +--- + +## 2. PROJEKT-STRUKTUR + +``` +/app +├── (dashboard) +│ ├── layout.tsx # Dashboard-Layout mit Sidebar +│ ├── page.tsx # Dashboard-Startseite +│ ├── agents/ +│ │ ├── page.tsx # Agent-Liste +│ │ └── [id]/page.tsx # Agent-Detail +│ ├── alerts/ +│ │ └── page.tsx # Alert-Center +│ ├── reports/ +│ │ └── page.tsx # Reports +│ └── settings/ +│ └── page.tsx # Einstellungen +├── api/ +│ ├── agents/route.ts # API: Agenten +│ ├── alerts/route.ts # API: Alerts +│ └── scores/route.ts # API: Scores +├── globals.css +└── layout.tsx + +/components +├── ui/ # shadcn/ui Komponenten +├── dashboard/ +│ ├── stat-card.tsx +│ ├── risk-badge.tsx +│ ├── agent-card.tsx +│ ├── alert-item.tsx +│ └── performance-chart.tsx +├── layout/ +│ ├── sidebar.tsx +│ ├── header.tsx +│ └── mobile-nav.tsx +└── shared/ + ├── data-table.tsx + ├── loading.tsx + └── empty-state.tsx + +/lib +├── api.ts # API-Client +├── utils.ts # Utility-Funktionen +├── types.ts # TypeScript-Typen +└── constants.ts # Konstanten + +/hooks +├── use-agents.ts +├── use-alerts.ts +├── use-websocket.ts +└── use-media-query.ts + +/stores +├── alert-store.ts +└── ui-store.ts +``` + +--- + +## 3. TYPESCRIPT-TYPEN + +```typescript +// lib/types.ts + +export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'; + +export interface Agent { + id: string; + name: string; + status: 'online' | 'offline' | 'paused' | 'review'; + supervisor?: string; + createdAt: string; +} + +export interface ScoreResult { + agent_id: string; + contact: string | null; + timestamp: string | null; + price_claim: boolean; + price_keywords_found: string[]; + legal_claim: boolean; + legal_keywords_found: string[]; + stop_triggered: boolean; + placeholder_used: boolean; + risk: number; + risk_level: RiskLevel; + violations: string[]; +} + +export interface AgentStatistics { + agent_id: string; + total_interactions: number; + average_risk: number; + price_claims: number; + legal_claims: number; + stops_triggered: number; + stop_rate: string; + critical_incidents: number; + risk_distribution: Record; +} + +export interface Alert { + id: string; + agent_id: string; + risk_level: RiskLevel; + violations: string[]; + transcript_snippet?: string; + timestamp: string; + read: boolean; + contact_name?: string; +} + +export interface DashboardSummary { + total: number; + average_risk: number; + risk_distribution: Record; + critical_count: number; + critical_incidents: Array<{ + agent_id: string; + risk_level: RiskLevel; + violations: string[]; + }>; + agents_analyzed: number; +} + +export interface SupervisorDashboard { + date: string; + agents_active: number; + total_interactions: number; + stopped_calls_today: number; + potential_issues: Array<{ + agent_id: string; + issue: string; + risk: RiskLevel; + timestamp?: string; + }>; + agents_requiring_review: Array<{ + agent_id: string; + average_risk: number; + critical_incidents: number; + recommendation: string; + }>; + action_required: boolean; + supervisor_recommendation: string; + summary: { + average_risk: number; + total_violations: number; + stop_compliance_rate: string; + }; +} +``` + +--- + +## 4. KOMPONENTEN-IMPLEMENTIERUNG + +### 4.1 Risk Badge + +```tsx +// components/dashboard/risk-badge.tsx +import { cva, type VariantProps } from 'class-variance-authority'; +import { CheckCircle, AlertTriangle, AlertOctagon, XCircle } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { RiskLevel } from '@/lib/types'; + +const badgeVariants = cva( + 'inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-semibold uppercase', + { + variants: { + risk: { + LOW: 'bg-green-100 text-green-800 border border-green-200', + MEDIUM: 'bg-yellow-100 text-yellow-800 border border-yellow-200', + HIGH: 'bg-orange-100 text-orange-800 border border-orange-200', + CRITICAL: 'bg-red-100 text-red-800 border border-red-200 animate-pulse', + }, + }, + defaultVariants: { + risk: 'LOW', + }, + } +); + +const icons: Record> = { + LOW: CheckCircle, + MEDIUM: AlertTriangle, + HIGH: AlertOctagon, + CRITICAL: XCircle, +}; + +interface RiskBadgeProps extends VariantProps { + level: RiskLevel; + showIcon?: boolean; + className?: string; +} + +export function RiskBadge({ level, showIcon = true, className }: RiskBadgeProps) { + const Icon = icons[level]; + + return ( + + {showIcon && + ); +} +``` + +### 4.2 Stat Card + +```tsx +// components/dashboard/stat-card.tsx +import { Card, CardContent } from '@/components/ui/card'; +import { TrendingUp, TrendingDown, Minus } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface StatCardProps { + title: string; + value: string | number; + trend?: number; + trendLabel?: string; + icon?: React.ReactNode; + className?: string; +} + +export function StatCard({ + title, + value, + trend, + trendLabel, + icon, + className, +}: StatCardProps) { + const getTrendIcon = () => { + if (!trend) return ; + if (trend > 0) return ; + return ; + }; + + const getTrendColor = () => { + if (!trend) return 'text-gray-500'; + return trend > 0 ? 'text-green-600' : 'text-red-600'; + }; + + return ( + + +
+

{title}

+ {icon &&
{icon}
} +
+
+

{value}

+ {trend !== undefined && ( +
+ {getTrendIcon()} + {Math.abs(trend)}% +
+ )} +
+ {trendLabel && ( +

{trendLabel}

+ )} +
+
+ ); +} +``` + +### 4.3 Agent Card + +```tsx +// components/dashboard/agent-card.tsx +import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { RiskBadge } from './risk-badge'; +import { Bot, Pause, Eye } from 'lucide-react'; +import type { Agent, AgentStatistics } from '@/lib/types'; + +interface AgentCardProps { + agent: Agent; + stats?: AgentStatistics; + onViewDetails?: () => void; + onPause?: () => void; +} + +export function AgentCard({ agent, stats, onViewDetails, onPause }: AgentCardProps) { + const getStatusColor = () => { + switch (agent.status) { + case 'online': return 'bg-green-500'; + case 'offline': return 'bg-gray-400'; + case 'paused': return 'bg-yellow-500'; + case 'review': return 'bg-red-500'; + default: return 'bg-gray-400'; + } + }; + + const riskLevel = stats?.average_risk + ? stats.average_risk <= 0 ? 'LOW' + : stats.average_risk <= 1 ? 'MEDIUM' + : stats.average_risk <= 2 ? 'HIGH' + : 'CRITICAL' + : 'LOW'; + + return ( + + +
+
+ + + + + +
+

{agent.id}

+

{agent.name}

+
+
+
+ + {agent.status} +
+
+
+ + + {stats && ( + <> +
+
+

+ {stats.total_interactions} +

+

Calls Today

+
+
+

+ {stats.average_risk.toFixed(1)} +

+

Avg. Risk

+
+
+

{stats.stop_rate}

+

Stop Rate

+
+
+ +
+

Risk Distribution

+
+ {(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] as const).map((level) => { + const count = stats.risk_distribution[level] || 0; + const total = Object.values(stats.risk_distribution).reduce((a, b) => a + b, 0); + const percentage = total > 0 ? (count / total) * 100 : 0; + + return ( +
+
{level}
+
+
+
+
+ {Math.round(percentage)}% +
+
+ ); + })} +
+
+ + )} + + + + + + + + ); +} +``` + +### 4.4 Alert Item + +```tsx +// components/dashboard/alert-item.tsx +import { Card, CardContent } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { RiskBadge } from './risk-badge'; +import { Eye, Pause, Check, Clock } from 'lucide-react'; +import { formatDistanceToNow } from 'date-fns'; +import { de } from 'date-fns/locale'; +import type { Alert } from '@/lib/types'; +import { cn } from '@/lib/utils'; + +interface AlertItemProps { + alert: Alert; + onView?: () => void; + onPauseAgent?: () => void; + onDismiss?: () => void; +} + +export function AlertItem({ alert, onView, onPauseAgent, onDismiss }: AlertItemProps) { + const borderColor = { + LOW: 'border-l-green-500', + MEDIUM: 'border-l-yellow-500', + HIGH: 'border-l-orange-500', + CRITICAL: 'border-l-red-500', + }[alert.risk_level]; + + const bgColor = { + LOW: 'bg-green-50/50', + MEDIUM: 'bg-yellow-50/50', + HIGH: 'bg-orange-50/50', + CRITICAL: 'bg-red-50/50', + }[alert.risk_level]; + + return ( + + +
+ +
+ + {formatDistanceToNow(new Date(alert.timestamp), { + addSuffix: true, + locale: de, + })} +
+
+ +
+

+ {alert.agent_id} + {alert.contact_name && ( + - {alert.contact_name} + )} +

+
+ + {alert.violations.length > 0 && ( +
    + {alert.violations.map((violation, i) => ( +
  • + • + {violation} +
  • + ))} +
+ )} + + {alert.transcript_snippet && ( +
+ "{alert.transcript_snippet}" +
+ )} + +
+ + {alert.risk_level === 'CRITICAL' && ( + + )} + +
+
+
+ ); +} +``` + +### 4.5 Performance Chart + +```tsx +// components/dashboard/performance-chart.tsx +'use client'; + +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, +} from 'recharts'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +interface DataPoint { + date: string; + avgRisk: number; + criticalIncidents: number; + stopRate: number; +} + +interface PerformanceChartProps { + data: DataPoint[]; + title?: string; +} + +export function PerformanceChart({ + data, + title = 'Performance Trend', +}: PerformanceChartProps) { + return ( + + + {title} + + +
+ + + + + + + + + + + + +
+
+
+ ); +} +``` + +--- + +## 5. LAYOUT-KOMPONENTEN + +### 5.1 Sidebar + +```tsx +// components/layout/sidebar.tsx +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { + LayoutDashboard, + Bot, + Bell, + BarChart3, + Settings, + ChevronLeft, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { useState } from 'react'; + +const navigation = [ + { name: 'Dashboard', href: '/', icon: LayoutDashboard }, + { name: 'Agents', href: '/agents', icon: Bot }, + { name: 'Alerts', href: '/alerts', icon: Bell }, + { name: 'Reports', href: '/reports', icon: BarChart3 }, + { name: 'Settings', href: '/settings', icon: Settings }, +]; + +export function Sidebar() { + const pathname = usePathname(); + const [collapsed, setCollapsed] = useState(false); + + return ( + + ); +} +``` + +### 5.2 Header + +```tsx +// components/layout/header.tsx +'use client'; + +import { Bell, User, Search } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { useAlertStore } from '@/stores/alert-store'; + +export function Header() { + const unreadCount = useAlertStore((state) => state.unreadCount); + + return ( +
+ {/* Search */} +
+
+ + +
+
+ + {/* Actions */} +
+ {/* Notifications */} + + + {/* User Menu */} + + + + + + Profile + Settings + Logout + + +
+
+ ); +} +``` + +--- + +## 6. HOOKS & STATE + +### 6.1 Alert Store (Zustand) + +```typescript +// stores/alert-store.ts +import { create } from 'zustand'; +import type { Alert } from '@/lib/types'; + +interface AlertState { + alerts: Alert[]; + unreadCount: number; + addAlert: (alert: Alert) => void; + markAsRead: (id: string) => void; + markAllAsRead: () => void; + dismissAlert: (id: string) => void; +} + +export const useAlertStore = create((set) => ({ + alerts: [], + unreadCount: 0, + + addAlert: (alert) => + set((state) => ({ + alerts: [alert, ...state.alerts], + unreadCount: state.unreadCount + 1, + })), + + markAsRead: (id) => + set((state) => ({ + alerts: state.alerts.map((a) => + a.id === id ? { ...a, read: true } : a + ), + unreadCount: Math.max(0, state.unreadCount - 1), + })), + + markAllAsRead: () => + set((state) => ({ + alerts: state.alerts.map((a) => ({ ...a, read: true })), + unreadCount: 0, + })), + + dismissAlert: (id) => + set((state) => ({ + alerts: state.alerts.filter((a) => a.id !== id), + unreadCount: state.alerts.find((a) => a.id === id && !a.read) + ? state.unreadCount - 1 + : state.unreadCount, + })), +})); +``` + +### 6.2 useAgents Hook + +```typescript +// hooks/use-agents.ts +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { api } from '@/lib/api'; +import type { Agent, AgentStatistics } from '@/lib/types'; + +export function useAgents() { + return useQuery({ + queryKey: ['agents'], + queryFn: () => api.get('/api/agents'), + refetchInterval: 30000, // Refresh every 30s + }); +} + +export function useAgentStats(agentId: string) { + return useQuery({ + queryKey: ['agent-stats', agentId], + queryFn: () => api.get(`/api/agents/${agentId}/stats`), + enabled: !!agentId, + }); +} + +export function usePauseAgent() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (agentId: string) => + api.post(`/api/agents/${agentId}/pause`), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['agents'] }); + }, + }); +} +``` + +### 6.3 useWebSocket Hook + +```typescript +// hooks/use-websocket.ts +import { useEffect, useRef, useCallback } from 'react'; +import { useAlertStore } from '@/stores/alert-store'; +import type { Alert } from '@/lib/types'; + +interface WebSocketMessage { + type: 'alert.new' | 'agent.status' | 'score.update'; + payload: any; +} + +export function useWebSocket(url: string) { + const wsRef = useRef(null); + const addAlert = useAlertStore((state) => state.addAlert); + + const connect = useCallback(() => { + const ws = new WebSocket(url); + + ws.onopen = () => { + console.log('WebSocket connected'); + }; + + ws.onmessage = (event) => { + const message: WebSocketMessage = JSON.parse(event.data); + + switch (message.type) { + case 'alert.new': + addAlert(message.payload as Alert); + // Show toast notification + if (message.payload.risk_level === 'CRITICAL') { + // Trigger toast + } + break; + case 'agent.status': + // Update agent status in query cache + break; + case 'score.update': + // Update dashboard stats + break; + } + }; + + ws.onclose = () => { + console.log('WebSocket disconnected, reconnecting...'); + setTimeout(connect, 3000); + }; + + wsRef.current = ws; + }, [url, addAlert]); + + useEffect(() => { + connect(); + return () => { + wsRef.current?.close(); + }; + }, [connect]); + + return wsRef.current; +} +``` + +--- + +## 7. API-CLIENT + +```typescript +// lib/api.ts +const BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; + +class ApiClient { + private baseUrl: string; + + constructor(baseUrl: string) { + this.baseUrl = baseUrl; + } + + private async request( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${this.baseUrl}${endpoint}`; + + const response = await fetch(url, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }); + + if (!response.ok) { + throw new Error(`API Error: ${response.status}`); + } + + return response.json(); + } + + get(endpoint: string): Promise { + return this.request(endpoint); + } + + post(endpoint: string, data?: any): Promise { + return this.request(endpoint, { + method: 'POST', + body: data ? JSON.stringify(data) : undefined, + }); + } + + put(endpoint: string, data: any): Promise { + return this.request(endpoint, { + method: 'PUT', + body: JSON.stringify(data), + }); + } + + delete(endpoint: string): Promise { + return this.request(endpoint, { method: 'DELETE' }); + } +} + +export const api = new ApiClient(BASE_URL); +``` + +--- + +## 8. TAILWIND KONFIGURATION + +```typescript +// tailwind.config.ts +import type { Config } from 'tailwindcss'; + +const config: Config = { + darkMode: ['class'], + content: [ + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + ], + theme: { + extend: { + colors: { + primary: { + 50: '#EEF2FF', + 100: '#E0E7FF', + 200: '#C7D2FE', + 300: '#A5B4FC', + 400: '#818CF8', + 500: '#6366F1', + 600: '#4F46E5', + 700: '#4338CA', + 800: '#3730A3', + 900: '#312E81', + }, + risk: { + low: '#10B981', + medium: '#F59E0B', + high: '#F97316', + critical: '#EF4444', + }, + }, + animation: { + 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', + }, + }, + }, + plugins: [require('tailwindcss-animate')], +}; + +export default config; +``` + +--- + +## 9. NÄCHSTE SCHRITTE + +1. **Phase 1**: Projekt-Setup und Design-System +2. **Phase 2**: Basis-Komponenten implementieren +3. **Phase 3**: Layout und Navigation +4. **Phase 4**: Dashboard-Seite +5. **Phase 5**: Agent-Management +6. **Phase 6**: Alert-System mit WebSocket +7. **Phase 7**: Reports und Export +8. **Phase 8**: Testing und Optimierung + +--- + +**Dokument-Version:** 1.0 +**Erstellt:** 23.12.2025 From 3ed37a1c1e71a898c0a1661819a5db6996f1beb1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Dec 2025 20:56:36 +0000 Subject: [PATCH 2/4] Add supervised system baseline with Engineering Lead Supervisor Implements a complete supervised AI system with: - Engineering Lead Supervisor (plan, delegate, verify, STOP) - Cloud Assistant (execute under supervision with evidence) - Meta Supervisor (routing and monitoring) - STOP scoring system with configurable thresholds - Minimal Node/TS API service with Express - SQLite persistence with in-memory fallback - In-memory queue adapter (BullMQ scaffold) - Integration stubs (WhatsApp, Voice, Google, iCloud, Pinecone) - Docker Compose setup with optional Redis - Comprehensive documentation (prompts, policies, runbooks, architecture) All integrations are stubs - no live connections without explicit approval. Pricing and legal facts marked as UNKNOWN per policy. --- .dockerignore | 11 + .env.example | 36 + .gitignore | 35 +- Dockerfile | 40 + docker-compose.yml | 32 + docs/architecture/decision-flow.md | 157 ++ docs/architecture/system-overview.md | 139 ++ docs/assistants/cloud_assistant.system.md | 50 + docs/facts/company.md | 6 + docs/facts/countries.md | 3 + docs/facts/pricing.md | 3 + docs/facts/products.md | 3 + docs/policies/default-policy.yaml | 24 + docs/runbooks/live-runbook.md | 58 + docs/runbooks/production-launch-checklist.md | 40 + .../engineering_lead_supervisor.system.md | 46 + docs/supervisor/meta_supervisor.system.md | 39 + package-lock.json | 1961 +++++++++++++++++ package.json | 32 + src/api/audit.ts | 51 + src/api/health.ts | 40 + src/api/tasks.ts | 80 + src/assistant/cloudAssistant.ts | 181 ++ src/audit/decision.schema.json | 60 + src/audit/stopScorer.ts | 124 ++ src/db/database.ts | 191 ++ src/index.ts | 61 + src/integrations/google/client.ts | 88 + src/integrations/icloud/client.ts | 84 + src/integrations/index.ts | 40 + src/integrations/pinecone/client.ts | 82 + src/integrations/voice/client.ts | 72 + src/integrations/whatsapp/client.ts | 65 + src/meta/metaSupervisor.ts | 165 ++ src/queue/queue.ts | 118 + src/supervisor/engineeringLead.ts | 158 ++ tests/stopScorer.test.ts | 81 + tsconfig.json | 19 + 38 files changed, 4471 insertions(+), 4 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 docs/architecture/decision-flow.md create mode 100644 docs/architecture/system-overview.md create mode 100644 docs/assistants/cloud_assistant.system.md create mode 100644 docs/facts/company.md create mode 100644 docs/facts/countries.md create mode 100644 docs/facts/pricing.md create mode 100644 docs/facts/products.md create mode 100644 docs/policies/default-policy.yaml create mode 100644 docs/runbooks/live-runbook.md create mode 100644 docs/runbooks/production-launch-checklist.md create mode 100644 docs/supervisor/engineering_lead_supervisor.system.md create mode 100644 docs/supervisor/meta_supervisor.system.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/api/audit.ts create mode 100644 src/api/health.ts create mode 100644 src/api/tasks.ts create mode 100644 src/assistant/cloudAssistant.ts create mode 100644 src/audit/decision.schema.json create mode 100644 src/audit/stopScorer.ts create mode 100644 src/db/database.ts create mode 100644 src/index.ts create mode 100644 src/integrations/google/client.ts create mode 100644 src/integrations/icloud/client.ts create mode 100644 src/integrations/index.ts create mode 100644 src/integrations/pinecone/client.ts create mode 100644 src/integrations/voice/client.ts create mode 100644 src/integrations/whatsapp/client.ts create mode 100644 src/meta/metaSupervisor.ts create mode 100644 src/queue/queue.ts create mode 100644 src/supervisor/engineeringLead.ts create mode 100644 tests/stopScorer.test.ts create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..822fe49 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +node_modules +dist +data +*.log +.env +.git +.gitignore +README.md +docs +tests +*.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..95ef558 --- /dev/null +++ b/.env.example @@ -0,0 +1,36 @@ +# Server +PORT=3000 +NODE_ENV=development + +# Database +SQLITE_PATH=./data/app.sqlite + +# Queue (optional - uses in-memory fallback if not set) +REDIS_URL=redis://localhost:6379 +QUEUE_ENABLED=false + +# Integrations (all stubs - do not enable without explicit approval) +WHATSAPP_ENABLED=false +WHATSAPP_API_URL= +WHATSAPP_API_TOKEN= + +VOICE_ENABLED=false +VOICE_PROVIDER= +VOICE_API_KEY= + +GOOGLE_ENABLED=false +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +ICLOUD_ENABLED=false +ICLOUD_APPLE_ID= +ICLOUD_APP_PASSWORD= + +PINECONE_ENABLED=false +PINECONE_API_KEY= +PINECONE_ENVIRONMENT= +PINECONE_INDEX= + +# Supervisor +STOP_SCORE_THRESHOLD=40 +MAX_PARALLEL_AGENTS=4 diff --git a/.gitignore b/.gitignore index 5d82a5f..c3f1adc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,34 @@ -__pycache__/ -*.log +# Dependencies +node_modules/ + +# Build output +dist/ + +# Data +data/ + +# Environment .env -*.pyc +.env.local +.env.*.local + +# Logs +*.log +npm-debug.log* + +# IDE .idea/ .vscode/ -secrets.json \ No newline at end of file +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Test coverage +coverage/ + +# Temporary files +*.tmp +*.temp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..87168e5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ +COPY tsconfig.json ./ + +# Install dependencies +RUN npm ci + +# Copy source +COPY src ./src + +# Build +RUN npm run build + +# Production stage +FROM node:20-alpine + +WORKDIR /app + +# Copy package files and install production dependencies +COPY package*.json ./ +RUN npm ci --omit=dev + +# Copy built files +COPY --from=builder /app/dist ./dist + +# Create data directory +RUN mkdir -p /app/data + +# Set environment +ENV NODE_ENV=production +ENV PORT=3000 + +EXPOSE 3000 + +CMD ["node", "dist/index.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..be03453 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +version: "3.8" + +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "3000:3000" + environment: + - NODE_ENV=development + - PORT=3000 + - SQLITE_PATH=/app/data/app.sqlite + - REDIS_URL=redis://redis:6379 + - QUEUE_ENABLED=false + volumes: + - ./data:/app/data + depends_on: + - redis + restart: unless-stopped + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + restart: unless-stopped + +volumes: + redis_data: diff --git a/docs/architecture/decision-flow.md b/docs/architecture/decision-flow.md new file mode 100644 index 0000000..9c4c0f6 --- /dev/null +++ b/docs/architecture/decision-flow.md @@ -0,0 +1,157 @@ +# Decision Flow + +## STOP Score Calculation + +``` +┌─────────────────────────────────────────────────────────────┐ +│ INPUT: Work Submission │ +│ - Description │ +│ - Claims │ +│ - Artefacts │ +│ - Test status │ +│ - Schema status │ +│ - Deploy config status │ +└─────────────────────────────┬───────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ REASON DETECTION │ +│ │ +│ PRICING_WITHOUT_FACT → +40 points │ +│ LEGAL_STATEMENT → +40 points │ +│ UNPROVEN_CLAIM → +30 points │ +│ CROSS_LAYER_MISMATCH → +25 points │ +│ MISSING_SQL_OR_SCHEMA → +25 points │ +│ COST_OR_LOAD_RISK → +20 points │ +│ MISSING_TESTS → +15 points │ +│ MISSING_DEPLOY_CONFIG → +15 points │ +│ │ +│ Maximum Score: 100 │ +└─────────────────────────────┬───────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ SEVERITY MAPPING │ +│ │ +│ 0-19: LOW │ +│ 20-44: MEDIUM │ +│ 45-69: HIGH │ +│ 70-100: CRITICAL │ +│ │ +│ STOP Required: score >= 40 │ +└─────────────────────────────┬───────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ FINAL DECISION │ +│ │ +│ score < 40 → APPROVED │ +│ score >= 40 → STOP_REQUIRED │ +│ │ +│ Final Status: │ +│ - COMPLETE: No issues │ +│ - COMPLETE_WITH_GAPS: Issues but not blocking │ +│ - STOP_REQUIRED: Must address before proceeding │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Decision Examples + +### Example 1: Clean Submission +``` +Input: + - Description: "Added user login endpoint" + - Artefacts: [src/api/auth.ts, tests/auth.test.ts] + - hasTests: true + - hasSchema: true + - hasDeployConfig: true + +Detected Reasons: [] +Score: 0 +Severity: LOW + +Decision: APPROVED +Final Status: COMPLETE +``` + +### Example 2: Missing Tests +``` +Input: + - Description: "Added payment processing" + - Artefacts: [src/api/payments.ts] + - hasTests: false + - hasSchema: true + - hasDeployConfig: false + +Detected Reasons: [MISSING_TESTS, MISSING_DEPLOY_CONFIG] +Score: 30 +Severity: MEDIUM + +Decision: APPROVED +Final Status: COMPLETE_WITH_GAPS +Required Action: Add missing: Tests, Deploy config +``` + +### Example 3: Pricing Claim +``` +Input: + - Description: "Set price to $99/month for premium tier" + - Artefacts: [src/pricing.ts] + - hasTests: true + - hasSchema: true + - hasDeployConfig: true + +Detected Reasons: [PRICING_WITHOUT_FACT] +Score: 40 +Severity: HIGH + +Decision: STOP_REQUIRED +Final Status: STOP_REQUIRED +Required Action: STOP: Address PRICING_WITHOUT_FACT before proceeding +``` + +### Example 4: Multiple Issues +``` +Input: + - Description: "Deployed the API with legal disclaimer" + - Artefacts: [] + - hasTests: false + - hasSchema: false + - hasDeployConfig: false + +Detected Reasons: [ + UNPROVEN_CLAIM (deployed), + LEGAL_STATEMENT, + MISSING_TESTS, + MISSING_SQL_OR_SCHEMA, + MISSING_DEPLOY_CONFIG +] +Score: 100 (capped) +Severity: CRITICAL + +Decision: STOP_REQUIRED +Final Status: STOP_REQUIRED +Required Action: STOP: Address all issues before proceeding +``` + +## Escalation Path + +``` +Cloud Assistant + │ + │ submits work + ▼ +Engineering Lead Supervisor + │ + │ STOP_REQUIRED? + ├──────────────────┐ + │ Yes │ No + ▼ ▼ +Meta Supervisor Continue + │ + │ Cross-system? + ├──────────────────┐ + │ Yes │ No + ▼ ▼ +Human Review Log & Monitor +``` diff --git a/docs/architecture/system-overview.md b/docs/architecture/system-overview.md new file mode 100644 index 0000000..ef02084 --- /dev/null +++ b/docs/architecture/system-overview.md @@ -0,0 +1,139 @@ +# System Architecture Overview + +## Core Components + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ META SUPERVISOR │ +│ (Routing + Monitoring + Aggregation) │ +└─────────────────────────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ ENGINEERING LEAD SUPERVISOR │ +│ (Plan + Delegate + Verify + STOP) │ +│ │ +│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌───────────┐ │ +│ │ STOP │ │ Artefact │ │ Cross- │ │ Risk │ │ +│ │ Scorer │ │ Verifier │ │ Layer │ │ Assessor │ │ +│ │ │ │ │ │ Check │ │ │ │ +│ └────────────┘ └────────────┘ └────────────┘ └───────────┘ │ +└─────────────────────────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ CLOUD ASSISTANT │ +│ (Execute + Report + Evidence) │ +│ │ +│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌───────────┐ │ +│ │ Code │ │ Config │ │ Test │ │ Evidence │ │ +│ │ Writer │ │ Manager │ │ Runner │ │ Collector │ │ +│ │ │ │ │ │ │ │ │ │ +│ └────────────┘ └────────────┘ └────────────┘ └───────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Data Flow + +1. **Task Submission** → API receives task +2. **Routing** → Meta Supervisor routes to appropriate system +3. **Planning** → Engineering Lead plans execution +4. **Execution** → Cloud Assistant executes with evidence +5. **Verification** → Engineering Lead verifies artefacts +6. **Decision** → APPROVED or STOP_REQUIRED +7. **Audit** → Decision logged for review + +## Key Principles + +### 1. Evidence-Based Verification +- No claim accepted without proof +- Every artefact must be verifiable +- File paths, test outputs, configs required + +### 2. STOP is Success +- When truth or safety at risk, STOP is correct +- STOP decisions are never overridden by lower layers +- Escalation path: Assistant → Lead → Meta → Human + +### 3. Cross-Layer Consistency +- Frontend ↔ Backend alignment required +- Backend ↔ Database schema match +- Claims ↔ Artefacts correspondence + +## Component Details + +### Meta Supervisor +- Routes tasks to systems +- Monitors health metrics +- Aggregates costs and outcomes +- Does NOT make business decisions + +### Engineering Lead Supervisor +- Plans implementation approach +- Delegates to Cloud Assistant +- Verifies completed work +- Makes APPROVE/STOP decisions + +### Cloud Assistant +- Executes code changes +- Collects evidence +- Reports status honestly +- Never declares "done" without proof + +## Integration Points + +All integrations are **STUBS** until explicitly approved: + +| Integration | Status | Notes | +|-------------|--------|-------| +| WhatsApp | STUB | Requires approval | +| Voice | STUB | Requires approval | +| Google | STUB | OAuth not configured | +| iCloud | STUB | Requires approval | +| Pinecone | STUB | Vector DB not connected | + +## Database Schema + +```sql +-- Tasks table +CREATE TABLE tasks ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + description TEXT, + priority TEXT DEFAULT 'medium', + status TEXT DEFAULT 'pending', + assignee TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT, + stop_score INTEGER +); + +-- Audit entries table +CREATE TABLE audit_entries ( + id TEXT PRIMARY KEY, + task_id TEXT, + decision TEXT NOT NULL, + final_status TEXT NOT NULL, + risk_level TEXT NOT NULL, + stop_score INTEGER NOT NULL, + verified_artefacts TEXT NOT NULL, + missing_invalid_parts TEXT NOT NULL, + required_next_action TEXT NOT NULL, + created_at TEXT NOT NULL +); +``` + +## API Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/` | GET | Service info | +| `/health` | GET | Health check | +| `/health/ready` | GET | Readiness probe | +| `/health/live` | GET | Liveness probe | +| `/api/tasks` | GET | List tasks | +| `/api/tasks` | POST | Create task | +| `/api/tasks/:id` | GET | Get task | +| `/api/audit` | GET | List audit entries | +| `/api/audit/:id` | GET | Get audit entry | +| `/api/audit/stats/stop-scores` | GET | Stop score statistics | diff --git a/docs/assistants/cloud_assistant.system.md b/docs/assistants/cloud_assistant.system.md new file mode 100644 index 0000000..256e94e --- /dev/null +++ b/docs/assistants/cloud_assistant.system.md @@ -0,0 +1,50 @@ +ROLE: CLOUD_ASSISTANT (SUPERVISED) +AUTHORITY: SUBORDINATE TO ENGINEERING_LEAD_SUPERVISOR +MODE: EXECUTE + REPORT + EVIDENCE + +You execute engineering tasks (code, files, configs, integrations) under supervision. +You NEVER declare "done" without artefacts. +You ALWAYS provide evidence: file paths, snippets, commands, test output references. + +## Rules + +1. **No hallucinations** - If you cannot verify, state `UNKNOWN` and request supervisor decision +2. **No pricing/legal statements** - Treat as `UNKNOWN` +3. **Evidence required for claims** - e.g., "scraped 10 portals" must include exact list and code paths +4. **Always include**: + - What changed + - Where (file paths) + - How to run + - How to test + - Known gaps + +## Evidence Types + +- File paths with line numbers +- Code snippets +- Command outputs +- Test results +- Configuration values +- Schema definitions + +## Output Format + +``` +Status Proposal: COMPLETE | COMPLETE_WITH_GAPS | STOP_REQUIRED +Evidence: + - [artefact 1] + - [artefact 2] +Risks/Gaps: + - [risk 1] + - [gap 1] +Next Steps: + - [step 1] + - [step 2] +``` + +## Interaction with Supervisor + +- Submit work with evidence +- Accept STOP decisions without argument +- Request clarification when requirements are ambiguous +- Report blockers immediately diff --git a/docs/facts/company.md b/docs/facts/company.md new file mode 100644 index 0000000..b2c61c1 --- /dev/null +++ b/docs/facts/company.md @@ -0,0 +1,6 @@ +# Company FACTS + +- Name: UNKNOWN +- Business type: UNKNOWN +- Target customers: UNKNOWN +- Languages: German, English, Bosnian, Serbian (planned) diff --git a/docs/facts/countries.md b/docs/facts/countries.md new file mode 100644 index 0000000..4413998 --- /dev/null +++ b/docs/facts/countries.md @@ -0,0 +1,3 @@ +# Countries FACTS + +- Primary country: UNKNOWN diff --git a/docs/facts/pricing.md b/docs/facts/pricing.md new file mode 100644 index 0000000..bb90a68 --- /dev/null +++ b/docs/facts/pricing.md @@ -0,0 +1,3 @@ +# Pricing FACTS + +- Pricing: UNKNOWN (requires explicit approval) diff --git a/docs/facts/products.md b/docs/facts/products.md new file mode 100644 index 0000000..4d04921 --- /dev/null +++ b/docs/facts/products.md @@ -0,0 +1,3 @@ +# Products FACTS + +- Offered services: Cloud-based supervised AI system with an Engineering Lead Supervisor and a Cloud Assistant for controlled multi-agent workflows. diff --git a/docs/policies/default-policy.yaml b/docs/policies/default-policy.yaml new file mode 100644 index 0000000..5d7f442 --- /dev/null +++ b/docs/policies/default-policy.yaml @@ -0,0 +1,24 @@ +policy_id: default +version: "1.0.0" + +rules: + pricing: + allowed: false + on_violation: STOP_REQUIRED + + legal: + allowed: false + on_violation: STOP_REQUIRED + + arena_parallel: + allowed: true + max_parallel_agents: 4 + + integrations_live: + allowed: false + comment: "All integrations are stubs until explicitly enabled" + +enforcement: + on_violation: STOP_REQUIRED + escalation: human_review + logging: required diff --git a/docs/runbooks/live-runbook.md b/docs/runbooks/live-runbook.md new file mode 100644 index 0000000..06488b9 --- /dev/null +++ b/docs/runbooks/live-runbook.md @@ -0,0 +1,58 @@ +# Live Runbook (Incident / Failure) + +## Golden Rule + +**STOP is success when safety or truth is at risk.** + +## Triggers → Actions + +| Trigger | Action | +|---------|--------| +| Pricing/legal mentioned | STOP_REQUIRED, escalate to human | +| Claims without proof | STOP_REQUIRED, demand artefacts | +| Queue overload / degraded health | Switch to DEGRADED mode, throttle tasks | +| Cost spike | Disable parallelization, enforce budgets | +| Integration failure | Fallback to stub, log error, continue | +| Database unavailable | Switch to in-memory mode, alert | + +## Evidence Policy + +**No evidence = not done.** + +All completed work must have: +- File paths +- Test results +- Command outputs +- Configuration proof + +## Escalation Path + +1. Assistant → Engineering Lead Supervisor +2. Engineering Lead → Meta Supervisor +3. Meta Supervisor → Human Review + +## Recovery Procedures + +### Queue Recovery +```bash +# Check queue status +npm run queue:status + +# Clear stuck jobs +npm run queue:clear-failed + +# Restart workers +npm run queue:restart +``` + +### Database Recovery +```bash +# Check DB health +npm run db:health + +# Run migrations +npm run db:migrate + +# Backup before recovery +npm run db:backup +``` diff --git a/docs/runbooks/production-launch-checklist.md b/docs/runbooks/production-launch-checklist.md new file mode 100644 index 0000000..b81c8c4 --- /dev/null +++ b/docs/runbooks/production-launch-checklist.md @@ -0,0 +1,40 @@ +# Production Launch Checklist + +## Before Go-Live + +- [ ] Supervisor prompts committed and versioned +- [ ] Facts reviewed (pricing remains UNKNOWN) +- [ ] Policy active and enforced +- [ ] Logging enabled (structured JSON) +- [ ] STOP scoring enabled +- [ ] Basic API health endpoint works +- [ ] DB migrations in place +- [ ] Queue adapter works (in-memory fallback ok) +- [ ] All integration stubs return graceful errors +- [ ] Environment variables documented + +## First Live Run + +- [ ] 1–5 tasks only +- [ ] No live messaging/voice integrations +- [ ] Review logs + stop scores +- [ ] Verify evidence collection +- [ ] Check error handling paths +- [ ] Monitor memory/CPU usage + +## Scaling Phase + +- [ ] KPI review (completion rate, stop rate) +- [ ] Cost review (API calls, compute) +- [ ] Supervisor approval for increased load +- [ ] Enable parallel processing (max 4 agents) +- [ ] Set up alerting thresholds + +## Integration Enablement + +For each integration: +- [ ] Security review completed +- [ ] Credentials rotated from dev +- [ ] Rate limits configured +- [ ] Fallback behavior tested +- [ ] Monitoring in place diff --git a/docs/supervisor/engineering_lead_supervisor.system.md b/docs/supervisor/engineering_lead_supervisor.system.md new file mode 100644 index 0000000..aee4f72 --- /dev/null +++ b/docs/supervisor/engineering_lead_supervisor.system.md @@ -0,0 +1,46 @@ +ROLE: ENGINEERING_LEAD_SUPERVISOR +AUTHORITY: ABSOLUTE +MODE: EXECUTION + CONTROL + AUDIT + +You are the leading Engineering Supervisor (Tech Lead + Architect + QA + Auditor). +You do NOT primarily write code. You PLAN, DELEGATE, VERIFY, and STOP. + +## Core Laws + +- No work accepted without verifiable artefacts (files, schemas, tests, configs) +- No statements trusted without proof +- No layer may be skipped: UI/UX, Frontend, Backend/API, Data/DB, Tests, Deploy, Docs +- Cross-layer consistency required (FE↔BE, BE↔DB, claims↔artefacts) +- If unclear/missing/inconsistent/unproven → STOP_REQUIRED + +## Forbidden Acceptance + +Never accept claims like: +- "implemented" +- "connected" +- "scraped 10 portals" +- "deployed" + +...without verifiable evidence. + +## Result States + +Exactly one of: +- `COMPLETE` +- `COMPLETE_WITH_GAPS` +- `STOP_REQUIRED` + +## Output Format + +``` +Decision: APPROVED | STOP_REQUIRED +Final Status: COMPLETE | COMPLETE_WITH_GAPS | STOP_REQUIRED +Risk Level: LOW | MEDIUM | HIGH | CRITICAL +Verified Artefacts: [list] +Missing/Invalid Parts: [list] +Required Next Action: [explicit instruction] +``` + +## Final Law + +**If work cannot be proven → it does not exist.** diff --git a/docs/supervisor/meta_supervisor.system.md b/docs/supervisor/meta_supervisor.system.md new file mode 100644 index 0000000..a3c96f6 --- /dev/null +++ b/docs/supervisor/meta_supervisor.system.md @@ -0,0 +1,39 @@ +ROLE: META_SUPERVISOR +AUTHORITY: ROUTING + MONITORING ONLY + +You route tasks to the correct supervisor repo/system. +You do not make business decisions and do not inject facts across boundaries. +You never override STOP decisions. +You aggregate health, cost, stop scores, and task outcomes. + +## Responsibilities + +1. **Routing**: Direct tasks to appropriate supervisor/system +2. **Monitoring**: Track health, costs, and outcomes +3. **Aggregation**: Collect metrics across systems +4. **Escalation**: Forward STOP decisions without override + +## Boundaries + +- NO business logic decisions +- NO fact injection across system boundaries +- NO overriding of STOP_REQUIRED decisions +- NO direct task execution + +## Output Format + +``` +Route Decision: [target_system] +Reason: [why this target] +Constraints: [any limits or conditions] +Monitoring Notes: [health/cost/status observations] +``` + +## Health Aggregation + +Track and report: +- Task completion rates +- STOP frequency and reasons +- Cost accumulation +- Queue depths +- Error rates diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..684c6af --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1961 @@ +{ + "name": "code-cloud-agents", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "code-cloud-agents", + "version": "0.1.0", + "dependencies": { + "better-sqlite3": "^11.0.0", + "express": "^4.19.2", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.11", + "@types/express": "^4.17.21", + "@types/node": "^20.12.12", + "tsx": "^4.16.2", + "typescript": "^5.5.4" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5fd189d --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "code-cloud-agents", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Supervised AI system with Engineering Lead Supervisor and Cloud Assistant", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/index.js", + "test": "node --test tests/**/*.test.ts", + "db:migrate": "tsx src/db/migrate.ts", + "db:health": "tsx src/db/health.ts", + "queue:status": "tsx src/queue/status.ts", + "lint": "echo 'No linter configured yet'" + }, + "dependencies": { + "better-sqlite3": "^11.0.0", + "express": "^4.19.2", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.11", + "@types/express": "^4.17.21", + "@types/node": "^20.12.12", + "tsx": "^4.16.2", + "typescript": "^5.5.4" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/src/api/audit.ts b/src/api/audit.ts new file mode 100644 index 0000000..2a2e075 --- /dev/null +++ b/src/api/audit.ts @@ -0,0 +1,51 @@ +/** + * Audit Router - View audit logs and supervisor decisions + */ + +import { Router } from "express"; +import type { Database } from "../db/database.js"; + +export function createAuditRouter(db: Database): Router { + const router = Router(); + + // List audit entries + router.get("/", (req, res) => { + try { + const limit = Math.min(parseInt(req.query.limit as string) || 50, 100); + const entries = db.listAuditEntries(limit); + res.json({ entries, count: entries.length }); + } catch (error) { + console.error("Failed to list audit entries:", error); + res.status(500).json({ error: "Internal server error" }); + } + }); + + // Get audit entry by ID + router.get("/:id", (req, res) => { + try { + const entry = db.getAuditEntry(req.params.id); + + if (!entry) { + return res.status(404).json({ error: "Audit entry not found" }); + } + + res.json(entry); + } catch (error) { + console.error("Failed to get audit entry:", error); + res.status(500).json({ error: "Internal server error" }); + } + }); + + // Get stop score statistics + router.get("/stats/stop-scores", (_req, res) => { + try { + const stats = db.getStopScoreStats(); + res.json(stats); + } catch (error) { + console.error("Failed to get stop score stats:", error); + res.status(500).json({ error: "Internal server error" }); + } + }); + + return router; +} diff --git a/src/api/health.ts b/src/api/health.ts new file mode 100644 index 0000000..2f93d89 --- /dev/null +++ b/src/api/health.ts @@ -0,0 +1,40 @@ +/** + * Health Check Router + */ + +import { Router } from "express"; +import type { Database } from "../db/database.js"; +import type { QueueAdapter } from "../queue/queue.js"; + +export function createHealthRouter(db: Database, queue: QueueAdapter): Router { + const router = Router(); + + router.get("/", (_req, res) => { + const health = { + status: "healthy", + timestamp: new Date().toISOString(), + components: { + database: db.isHealthy() ? "healthy" : "unhealthy", + queue: queue.isHealthy() ? "healthy" : "degraded", + }, + mode: queue.mode, + }; + + const statusCode = health.components.database === "healthy" ? 200 : 503; + res.status(statusCode).json(health); + }); + + router.get("/ready", (_req, res) => { + if (db.isHealthy()) { + res.json({ ready: true }); + } else { + res.status(503).json({ ready: false, reason: "database unavailable" }); + } + }); + + router.get("/live", (_req, res) => { + res.json({ live: true }); + }); + + return router; +} diff --git a/src/api/tasks.ts b/src/api/tasks.ts new file mode 100644 index 0000000..4355d4f --- /dev/null +++ b/src/api/tasks.ts @@ -0,0 +1,80 @@ +/** + * Tasks Router - Create and manage supervised tasks + */ + +import { Router } from "express"; +import { z } from "zod"; +import type { Database } from "../db/database.js"; +import type { QueueAdapter } from "../queue/queue.js"; + +const CreateTaskSchema = z.object({ + title: z.string().min(1).max(200), + description: z.string().optional(), + priority: z.enum(["low", "medium", "high"]).default("medium"), + assignee: z.enum(["cloud_assistant"]).default("cloud_assistant"), +}); + +export function createTaskRouter(db: Database, queue: QueueAdapter): Router { + const router = Router(); + + // Create task + router.post("/", async (req, res) => { + try { + const parsed = CreateTaskSchema.safeParse(req.body); + + if (!parsed.success) { + return res.status(400).json({ + error: "Invalid request", + details: parsed.error.issues, + }); + } + + const task = db.createTask({ + ...parsed.data, + status: "pending", + created_at: new Date().toISOString(), + }); + + // Add to queue + await queue.add("process_task", { taskId: task.id }); + + res.status(201).json({ + id: task.id, + status: "pending", + message: "Task created and queued for processing", + }); + } catch (error) { + console.error("Failed to create task:", error); + res.status(500).json({ error: "Internal server error" }); + } + }); + + // List tasks + router.get("/", (_req, res) => { + try { + const tasks = db.listTasks(); + res.json({ tasks, count: tasks.length }); + } catch (error) { + console.error("Failed to list tasks:", error); + res.status(500).json({ error: "Internal server error" }); + } + }); + + // Get task by ID + router.get("/:id", (req, res) => { + try { + const task = db.getTask(req.params.id); + + if (!task) { + return res.status(404).json({ error: "Task not found" }); + } + + res.json(task); + } catch (error) { + console.error("Failed to get task:", error); + res.status(500).json({ error: "Internal server error" }); + } + }); + + return router; +} diff --git a/src/assistant/cloudAssistant.ts b/src/assistant/cloudAssistant.ts new file mode 100644 index 0000000..935b602 --- /dev/null +++ b/src/assistant/cloudAssistant.ts @@ -0,0 +1,181 @@ +/** + * Cloud Assistant Module + * Executes work under supervision with evidence collection + */ + +export interface TaskExecution { + taskId: string; + description: string; + steps: ExecutionStep[]; + artefacts: Artefact[]; + status: "in_progress" | "completed" | "blocked"; + risks: string[]; + gaps: string[]; +} + +export interface ExecutionStep { + description: string; + completed: boolean; + evidence?: string; +} + +export interface Artefact { + type: "file" | "config" | "test" | "command_output" | "schema"; + path: string; + description: string; +} + +export interface StatusProposal { + status: "COMPLETE" | "COMPLETE_WITH_GAPS" | "STOP_REQUIRED"; + evidence: Artefact[]; + risks: string[]; + gaps: string[]; + nextSteps: string[]; +} + +/** + * Cloud Assistant + * Executes engineering tasks and collects evidence + */ +export class CloudAssistant { + private currentExecution: TaskExecution | null = null; + + /** + * Start executing a task + */ + startTask(taskId: string, description: string): TaskExecution { + this.currentExecution = { + taskId, + description, + steps: [], + artefacts: [], + status: "in_progress", + risks: [], + gaps: [], + }; + + return this.currentExecution; + } + + /** + * Add an execution step + */ + addStep(description: string, evidence?: string): void { + if (!this.currentExecution) { + throw new Error("No task in progress"); + } + + this.currentExecution.steps.push({ + description, + completed: !!evidence, + evidence, + }); + } + + /** + * Record an artefact + */ + recordArtefact(artefact: Artefact): void { + if (!this.currentExecution) { + throw new Error("No task in progress"); + } + + this.currentExecution.artefacts.push(artefact); + } + + /** + * Record a risk + */ + recordRisk(risk: string): void { + if (!this.currentExecution) { + throw new Error("No task in progress"); + } + + this.currentExecution.risks.push(risk); + } + + /** + * Record a gap + */ + recordGap(gap: string): void { + if (!this.currentExecution) { + throw new Error("No task in progress"); + } + + this.currentExecution.gaps.push(gap); + } + + /** + * Complete the current task and generate status proposal + */ + complete(): StatusProposal { + if (!this.currentExecution) { + throw new Error("No task in progress"); + } + + const execution = this.currentExecution; + execution.status = "completed"; + + // Determine proposed status + let status: "COMPLETE" | "COMPLETE_WITH_GAPS" | "STOP_REQUIRED"; + if (execution.risks.length > 0) { + status = "STOP_REQUIRED"; + } else if (execution.gaps.length > 0) { + status = "COMPLETE_WITH_GAPS"; + } else { + status = "COMPLETE"; + } + + // Generate next steps + const nextSteps: string[] = []; + if (execution.gaps.length > 0) { + nextSteps.push(`Address gaps: ${execution.gaps.join(", ")}`); + } + if (execution.risks.length > 0) { + nextSteps.push(`Mitigate risks: ${execution.risks.join(", ")}`); + } + if (nextSteps.length === 0) { + nextSteps.push("Submit for supervisor review"); + } + + const proposal: StatusProposal = { + status, + evidence: execution.artefacts, + risks: execution.risks, + gaps: execution.gaps, + nextSteps, + }; + + this.currentExecution = null; + + return proposal; + } + + /** + * Mark task as blocked + */ + block(reason: string): StatusProposal { + if (!this.currentExecution) { + throw new Error("No task in progress"); + } + + this.currentExecution.status = "blocked"; + this.currentExecution.risks.push(reason); + + return this.complete(); + } + + /** + * Get current execution state + */ + getCurrentExecution(): TaskExecution | null { + return this.currentExecution; + } +} + +/** + * Create a Cloud Assistant instance + */ +export function createCloudAssistant(): CloudAssistant { + return new CloudAssistant(); +} diff --git a/src/audit/decision.schema.json b/src/audit/decision.schema.json new file mode 100644 index 0000000..150b6e3 --- /dev/null +++ b/src/audit/decision.schema.json @@ -0,0 +1,60 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "SupervisorDecision", + "description": "Schema for Engineering Lead Supervisor decisions", + "required": [ + "decision", + "final_status", + "risk_level", + "verified_artefacts", + "missing_invalid_parts", + "required_next_action" + ], + "properties": { + "decision": { + "type": "string", + "enum": ["APPROVED", "STOP_REQUIRED"], + "description": "The supervisor's decision" + }, + "final_status": { + "type": "string", + "enum": ["COMPLETE", "COMPLETE_WITH_GAPS", "STOP_REQUIRED"], + "description": "Final status of the work" + }, + "risk_level": { + "type": "string", + "enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"], + "description": "Assessed risk level" + }, + "stop_score": { + "type": "number", + "minimum": 0, + "maximum": 100, + "description": "Computed stop score (0-100)" + }, + "verified_artefacts": { + "type": "array", + "items": { "type": "string" }, + "description": "List of verified artefacts (files, tests, configs)" + }, + "missing_invalid_parts": { + "type": "array", + "items": { "type": "string" }, + "description": "List of missing or invalid parts" + }, + "required_next_action": { + "type": "string", + "description": "Explicit next action required" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "Decision timestamp" + }, + "supervisor_id": { + "type": "string", + "description": "Identifier of the supervisor making the decision" + } + } +} diff --git a/src/audit/stopScorer.ts b/src/audit/stopScorer.ts new file mode 100644 index 0000000..5af39dc --- /dev/null +++ b/src/audit/stopScorer.ts @@ -0,0 +1,124 @@ +/** + * STOP Scorer - Computes risk scores for supervisor decisions + */ + +export type StopReason = + | "PRICING_WITHOUT_FACT" + | "LEGAL_STATEMENT" + | "UNPROVEN_CLAIM" + | "CROSS_LAYER_MISMATCH" + | "MISSING_SQL_OR_SCHEMA" + | "MISSING_TESTS" + | "MISSING_DEPLOY_CONFIG" + | "COST_OR_LOAD_RISK"; + +export type Severity = "LOW" | "MEDIUM" | "HIGH" | "CRITICAL"; + +export interface StopScoreResult { + score: number; + severity: Severity; + stopRequired: boolean; + reasons: StopReason[]; +} + +const REASON_WEIGHTS: Record = { + PRICING_WITHOUT_FACT: 40, + LEGAL_STATEMENT: 40, + UNPROVEN_CLAIM: 30, + CROSS_LAYER_MISMATCH: 25, + MISSING_SQL_OR_SCHEMA: 25, + MISSING_TESTS: 15, + MISSING_DEPLOY_CONFIG: 15, + COST_OR_LOAD_RISK: 20, +}; + +/** + * Computes a stop score based on identified risk reasons + * @param reasons Array of stop reasons identified + * @returns StopScoreResult with score, severity, and stop requirement + */ +export function computeStopScore(reasons: StopReason[]): StopScoreResult { + let score = 0; + + for (const reason of reasons) { + score += REASON_WEIGHTS[reason] ?? 0; + } + + // Cap at 100 + if (score > 100) { + score = 100; + } + + // Determine severity + let severity: Severity; + if (score >= 70) { + severity = "CRITICAL"; + } else if (score >= 45) { + severity = "HIGH"; + } else if (score >= 20) { + severity = "MEDIUM"; + } else { + severity = "LOW"; + } + + // STOP required if score >= 40 + const stopRequired = score >= 40; + + return { + score, + severity, + stopRequired, + reasons, + }; +} + +/** + * Analyzes text content for potential stop reasons + * @param content Text content to analyze + * @returns Array of identified stop reasons + */ +export function analyzeContent(content: string): StopReason[] { + const reasons: StopReason[] = []; + const lowerContent = content.toLowerCase(); + + // Check for pricing claims + if ( + lowerContent.includes("price") || + lowerContent.includes("cost") || + lowerContent.includes("€") || + lowerContent.includes("$") + ) { + if (!lowerContent.includes("unknown")) { + reasons.push("PRICING_WITHOUT_FACT"); + } + } + + // Check for legal statements + if ( + lowerContent.includes("legal") || + lowerContent.includes("liability") || + lowerContent.includes("warranty") || + lowerContent.includes("guarantee") + ) { + reasons.push("LEGAL_STATEMENT"); + } + + // Check for unproven claims + const claimPatterns = [ + "implemented", + "deployed", + "connected", + "integrated", + "scraped", + "completed", + ]; + + for (const pattern of claimPatterns) { + if (lowerContent.includes(pattern) && !lowerContent.includes("file:")) { + reasons.push("UNPROVEN_CLAIM"); + break; + } + } + + return reasons; +} diff --git a/src/db/database.ts b/src/db/database.ts new file mode 100644 index 0000000..cd06076 --- /dev/null +++ b/src/db/database.ts @@ -0,0 +1,191 @@ +/** + * Database Module - SQLite persistence layer + */ + +import BetterSqlite3 from "better-sqlite3"; +import { randomUUID } from "crypto"; + +export interface Task { + id: string; + title: string; + description?: string; + priority: "low" | "medium" | "high"; + status: "pending" | "in_progress" | "completed" | "stopped"; + assignee: string; + created_at: string; + updated_at?: string; + stop_score?: number; +} + +export interface AuditEntry { + id: string; + task_id?: string; + decision: "APPROVED" | "STOP_REQUIRED"; + final_status: "COMPLETE" | "COMPLETE_WITH_GAPS" | "STOP_REQUIRED"; + risk_level: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL"; + stop_score: number; + verified_artefacts: string; + missing_invalid_parts: string; + required_next_action: string; + created_at: string; +} + +export interface Database { + isHealthy(): boolean; + createTask(task: Omit): Task; + getTask(id: string): Task | undefined; + listTasks(): Task[]; + updateTask(id: string, updates: Partial): Task | undefined; + createAuditEntry(entry: Omit): AuditEntry; + getAuditEntry(id: string): AuditEntry | undefined; + listAuditEntries(limit?: number): AuditEntry[]; + getStopScoreStats(): { total: number; stopped: number; avgScore: number }; +} + +export function initDatabase(): Database { + const dbPath = process.env.SQLITE_PATH ?? "./data/app.sqlite"; + + let db: BetterSqlite3.Database; + + try { + db = new BetterSqlite3(dbPath); + db.pragma("journal_mode = WAL"); + + // Create tables + db.exec(` + CREATE TABLE IF NOT EXISTS tasks ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + description TEXT, + priority TEXT NOT NULL DEFAULT 'medium', + status TEXT NOT NULL DEFAULT 'pending', + assignee TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT, + stop_score INTEGER + ); + + CREATE TABLE IF NOT EXISTS audit_entries ( + id TEXT PRIMARY KEY, + task_id TEXT, + decision TEXT NOT NULL, + final_status TEXT NOT NULL, + risk_level TEXT NOT NULL, + stop_score INTEGER NOT NULL, + verified_artefacts TEXT NOT NULL, + missing_invalid_parts TEXT NOT NULL, + required_next_action TEXT NOT NULL, + created_at TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); + CREATE INDEX IF NOT EXISTS idx_audit_task_id ON audit_entries(task_id); + `); + } catch (error) { + console.warn("SQLite unavailable, using in-memory fallback:", error); + db = new BetterSqlite3(":memory:"); + db.exec(` + CREATE TABLE tasks ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + description TEXT, + priority TEXT NOT NULL DEFAULT 'medium', + status TEXT NOT NULL DEFAULT 'pending', + assignee TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT, + stop_score INTEGER + ); + + CREATE TABLE audit_entries ( + id TEXT PRIMARY KEY, + task_id TEXT, + decision TEXT NOT NULL, + final_status TEXT NOT NULL, + risk_level TEXT NOT NULL, + stop_score INTEGER NOT NULL, + verified_artefacts TEXT NOT NULL, + missing_invalid_parts TEXT NOT NULL, + required_next_action TEXT NOT NULL, + created_at TEXT NOT NULL + ); + `); + } + + return { + isHealthy(): boolean { + try { + db.prepare("SELECT 1").get(); + return true; + } catch { + return false; + } + }, + + createTask(task: Omit): Task { + const id = randomUUID(); + const stmt = db.prepare(` + INSERT INTO tasks (id, title, description, priority, status, assignee, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + `); + stmt.run(id, task.title, task.description ?? null, task.priority, task.status, task.assignee, task.created_at); + return { id, ...task }; + }, + + getTask(id: string): Task | undefined { + const stmt = db.prepare("SELECT * FROM tasks WHERE id = ?"); + return stmt.get(id) as Task | undefined; + }, + + listTasks(): Task[] { + const stmt = db.prepare("SELECT * FROM tasks ORDER BY created_at DESC LIMIT 100"); + return stmt.all() as Task[]; + }, + + updateTask(id: string, updates: Partial): Task | undefined { + const current = this.getTask(id); + if (!current) return undefined; + + const updated = { ...current, ...updates, updated_at: new Date().toISOString() }; + const stmt = db.prepare(` + UPDATE tasks SET title = ?, description = ?, priority = ?, status = ?, updated_at = ?, stop_score = ? + WHERE id = ? + `); + stmt.run(updated.title, updated.description ?? null, updated.priority, updated.status, updated.updated_at, updated.stop_score ?? null, id); + return updated; + }, + + createAuditEntry(entry: Omit): AuditEntry { + const id = randomUUID(); + const created_at = new Date().toISOString(); + const stmt = db.prepare(` + INSERT INTO audit_entries (id, task_id, decision, final_status, risk_level, stop_score, verified_artefacts, missing_invalid_parts, required_next_action, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + stmt.run(id, entry.task_id ?? null, entry.decision, entry.final_status, entry.risk_level, entry.stop_score, entry.verified_artefacts, entry.missing_invalid_parts, entry.required_next_action, created_at); + return { id, created_at, ...entry }; + }, + + getAuditEntry(id: string): AuditEntry | undefined { + const stmt = db.prepare("SELECT * FROM audit_entries WHERE id = ?"); + return stmt.get(id) as AuditEntry | undefined; + }, + + listAuditEntries(limit = 50): AuditEntry[] { + const stmt = db.prepare("SELECT * FROM audit_entries ORDER BY created_at DESC LIMIT ?"); + return stmt.all(limit) as AuditEntry[]; + }, + + getStopScoreStats(): { total: number; stopped: number; avgScore: number } { + const totalStmt = db.prepare("SELECT COUNT(*) as count FROM audit_entries"); + const stoppedStmt = db.prepare("SELECT COUNT(*) as count FROM audit_entries WHERE decision = 'STOP_REQUIRED'"); + const avgStmt = db.prepare("SELECT AVG(stop_score) as avg FROM audit_entries"); + + const total = (totalStmt.get() as { count: number }).count; + const stopped = (stoppedStmt.get() as { count: number }).count; + const avgScore = (avgStmt.get() as { avg: number | null }).avg ?? 0; + + return { total, stopped, avgScore: Math.round(avgScore * 100) / 100 }; + }, + }; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..a9e1667 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,61 @@ +/** + * Code Cloud Agents - Main Entry Point + * Supervised AI system with Engineering Lead Supervisor and Cloud Assistant + */ + +import express from "express"; +import { createHealthRouter } from "./api/health.js"; +import { createTaskRouter } from "./api/tasks.js"; +import { createAuditRouter } from "./api/audit.js"; +import { initDatabase } from "./db/database.js"; +import { initQueue } from "./queue/queue.js"; + +const PORT = process.env.PORT ?? 3000; + +async function main() { + console.log("🚀 Starting Code Cloud Agents..."); + + // Initialize database + const db = initDatabase(); + console.log("✅ Database initialized"); + + // Initialize queue + const queue = initQueue(); + console.log("✅ Queue initialized (mode:", queue.mode, ")"); + + // Create Express app + const app = express(); + app.use(express.json()); + + // Mount routers + app.use("/health", createHealthRouter(db, queue)); + app.use("/api/tasks", createTaskRouter(db, queue)); + app.use("/api/audit", createAuditRouter(db)); + + // Root endpoint + app.get("/", (_req, res) => { + res.json({ + name: "code-cloud-agents", + version: "0.1.0", + status: "running", + supervisor: "ENGINEERING_LEAD_SUPERVISOR", + mode: "SUPERVISED", + }); + }); + + // Start server + app.listen(PORT, () => { + console.log(`✅ Server running on http://localhost:${PORT}`); + console.log("📋 Endpoints:"); + console.log(" GET / - Service info"); + console.log(" GET /health - Health check"); + console.log(" POST /api/tasks - Create task"); + console.log(" GET /api/tasks - List tasks"); + console.log(" GET /api/audit - Audit log"); + }); +} + +main().catch((error) => { + console.error("❌ Failed to start:", error); + process.exit(1); +}); diff --git a/src/integrations/google/client.ts b/src/integrations/google/client.ts new file mode 100644 index 0000000..5be3e5b --- /dev/null +++ b/src/integrations/google/client.ts @@ -0,0 +1,88 @@ +/** + * Google Integration - STUB + * This is a placeholder integration. Do not use in production without explicit approval. + */ + +export interface GoogleConfig { + clientId: string; + clientSecret: string; + redirectUri?: string; +} + +export interface GoogleClient { + isEnabled(): boolean; + getAuthUrl(): string; + handleCallback(code: string): Promise<{ success: boolean; tokens?: unknown; error?: string }>; + getCalendarEvents(calendarId: string): Promise<{ success: boolean; events?: unknown[]; error?: string }>; + getContacts(): Promise<{ success: boolean; contacts?: unknown[]; error?: string }>; + getStatus(): Promise<{ connected: boolean; error?: string }>; +} + +/** + * Creates a Google client stub + * All methods return stub responses until properly configured + */ +export function createGoogleClient(_config?: GoogleConfig): GoogleClient { + const enabled = process.env.GOOGLE_ENABLED === "true"; + + return { + isEnabled(): boolean { + return enabled; + }, + + getAuthUrl(): string { + return "STUB: Google OAuth not configured"; + }, + + async handleCallback(_code: string): Promise<{ success: boolean; tokens?: unknown; error?: string }> { + if (!enabled) { + return { + success: false, + error: "STUB: Google integration is disabled. Set GOOGLE_ENABLED=true to enable.", + }; + } + + return { + success: false, + error: "STUB: Google OAuth not implemented", + }; + }, + + async getCalendarEvents(_calendarId: string): Promise<{ success: boolean; events?: unknown[]; error?: string }> { + if (!enabled) { + return { + success: false, + error: "STUB: Google integration is disabled", + }; + } + + return { + success: false, + events: [], + error: "STUB: Google Calendar not implemented", + }; + }, + + async getContacts(): Promise<{ success: boolean; contacts?: unknown[]; error?: string }> { + if (!enabled) { + return { + success: false, + error: "STUB: Google integration is disabled", + }; + } + + return { + success: false, + contacts: [], + error: "STUB: Google Contacts not implemented", + }; + }, + + async getStatus(): Promise<{ connected: boolean; error?: string }> { + return { + connected: false, + error: "STUB: Google integration not implemented", + }; + }, + }; +} diff --git a/src/integrations/icloud/client.ts b/src/integrations/icloud/client.ts new file mode 100644 index 0000000..55c647b --- /dev/null +++ b/src/integrations/icloud/client.ts @@ -0,0 +1,84 @@ +/** + * iCloud Integration - STUB + * This is a placeholder integration. Do not use in production without explicit approval. + */ + +export interface ICloudConfig { + appleId: string; + appPassword: string; +} + +export interface ICloudClient { + isEnabled(): boolean; + authenticate(): Promise<{ success: boolean; error?: string }>; + getContacts(): Promise<{ success: boolean; contacts?: unknown[]; error?: string }>; + getCalendarEvents(): Promise<{ success: boolean; events?: unknown[]; error?: string }>; + getReminders(): Promise<{ success: boolean; reminders?: unknown[]; error?: string }>; + getStatus(): Promise<{ connected: boolean; error?: string }>; +} + +/** + * Creates an iCloud client stub + * All methods return stub responses until properly configured + */ +export function createICloudClient(_config?: ICloudConfig): ICloudClient { + const enabled = process.env.ICLOUD_ENABLED === "true"; + + return { + isEnabled(): boolean { + return enabled; + }, + + async authenticate(): Promise<{ success: boolean; error?: string }> { + if (!enabled) { + return { + success: false, + error: "STUB: iCloud integration is disabled. Set ICLOUD_ENABLED=true to enable.", + }; + } + + return { + success: false, + error: "STUB: iCloud authentication not implemented", + }; + }, + + async getContacts(): Promise<{ success: boolean; contacts?: unknown[]; error?: string }> { + if (!enabled) { + return { + success: false, + error: "STUB: iCloud integration is disabled", + }; + } + + return { + success: false, + contacts: [], + error: "STUB: iCloud Contacts not implemented", + }; + }, + + async getCalendarEvents(): Promise<{ success: boolean; events?: unknown[]; error?: string }> { + return { + success: false, + events: [], + error: "STUB: iCloud Calendar not implemented", + }; + }, + + async getReminders(): Promise<{ success: boolean; reminders?: unknown[]; error?: string }> { + return { + success: false, + reminders: [], + error: "STUB: iCloud Reminders not implemented", + }; + }, + + async getStatus(): Promise<{ connected: boolean; error?: string }> { + return { + connected: false, + error: "STUB: iCloud integration not implemented", + }; + }, + }; +} diff --git a/src/integrations/index.ts b/src/integrations/index.ts new file mode 100644 index 0000000..106532b --- /dev/null +++ b/src/integrations/index.ts @@ -0,0 +1,40 @@ +/** + * Integrations Index - Export all integration clients + * + * IMPORTANT: All integrations are STUBS until explicitly enabled. + * Do not use in production without explicit approval. + */ + +export { createWhatsAppClient, type WhatsAppClient, type WhatsAppMessage } from "./whatsapp/client.js"; +export { createVoiceClient, type VoiceClient, type VoiceCall } from "./voice/client.js"; +export { createGoogleClient, type GoogleClient } from "./google/client.js"; +export { createICloudClient, type ICloudClient } from "./icloud/client.js"; +export { createPineconeClient, type PineconeClient, type PineconeVector } from "./pinecone/client.js"; + +/** + * Get status of all integrations + */ +export async function getAllIntegrationStatus(): Promise> { + return { + whatsapp: { + enabled: process.env.WHATSAPP_ENABLED === "true", + connected: false, + }, + voice: { + enabled: process.env.VOICE_ENABLED === "true", + connected: false, + }, + google: { + enabled: process.env.GOOGLE_ENABLED === "true", + connected: false, + }, + icloud: { + enabled: process.env.ICLOUD_ENABLED === "true", + connected: false, + }, + pinecone: { + enabled: process.env.PINECONE_ENABLED === "true", + connected: false, + }, + }; +} diff --git a/src/integrations/pinecone/client.ts b/src/integrations/pinecone/client.ts new file mode 100644 index 0000000..46cd188 --- /dev/null +++ b/src/integrations/pinecone/client.ts @@ -0,0 +1,82 @@ +/** + * Pinecone Integration - STUB + * This is a placeholder integration. Do not use in production without explicit approval. + */ + +export interface PineconeConfig { + apiKey: string; + environment: string; + indexName: string; +} + +export interface PineconeVector { + id: string; + values: number[]; + metadata?: Record; +} + +export interface PineconeClient { + isEnabled(): boolean; + upsert(vectors: PineconeVector[]): Promise<{ success: boolean; upsertedCount?: number; error?: string }>; + query(vector: number[], topK?: number): Promise<{ success: boolean; matches?: unknown[]; error?: string }>; + deleteById(ids: string[]): Promise<{ success: boolean; error?: string }>; + getStatus(): Promise<{ connected: boolean; error?: string }>; +} + +/** + * Creates a Pinecone client stub + * All methods return stub responses until properly configured + */ +export function createPineconeClient(_config?: PineconeConfig): PineconeClient { + const enabled = process.env.PINECONE_ENABLED === "true"; + + return { + isEnabled(): boolean { + return enabled; + }, + + async upsert(_vectors: PineconeVector[]): Promise<{ success: boolean; upsertedCount?: number; error?: string }> { + if (!enabled) { + return { + success: false, + error: "STUB: Pinecone integration is disabled. Set PINECONE_ENABLED=true to enable.", + }; + } + + console.warn("Pinecone upsert called but not implemented"); + return { + success: false, + error: "STUB: Pinecone upsert not implemented", + }; + }, + + async query(_vector: number[], _topK = 10): Promise<{ success: boolean; matches?: unknown[]; error?: string }> { + if (!enabled) { + return { + success: false, + error: "STUB: Pinecone integration is disabled", + }; + } + + return { + success: false, + matches: [], + error: "STUB: Pinecone query not implemented", + }; + }, + + async deleteById(_ids: string[]): Promise<{ success: boolean; error?: string }> { + return { + success: false, + error: "STUB: Pinecone delete not implemented", + }; + }, + + async getStatus(): Promise<{ connected: boolean; error?: string }> { + return { + connected: false, + error: "STUB: Pinecone integration not implemented", + }; + }, + }; +} diff --git a/src/integrations/voice/client.ts b/src/integrations/voice/client.ts new file mode 100644 index 0000000..e4b7a9d --- /dev/null +++ b/src/integrations/voice/client.ts @@ -0,0 +1,72 @@ +/** + * Voice Integration - STUB + * This is a placeholder integration. Do not use in production without explicit approval. + */ + +export interface VoiceConfig { + provider: string; + apiKey: string; +} + +export interface VoiceCall { + to: string; + message: string; + language?: string; +} + +export interface VoiceClient { + isEnabled(): boolean; + makeCall(call: VoiceCall): Promise<{ success: boolean; callId?: string; error?: string }>; + transcribe(audioUrl: string): Promise<{ success: boolean; text?: string; error?: string }>; + getStatus(): Promise<{ connected: boolean; error?: string }>; +} + +/** + * Creates a Voice client stub + * All methods return stub responses until properly configured + */ +export function createVoiceClient(_config?: VoiceConfig): VoiceClient { + const enabled = process.env.VOICE_ENABLED === "true"; + + return { + isEnabled(): boolean { + return enabled; + }, + + async makeCall(_call: VoiceCall): Promise<{ success: boolean; callId?: string; error?: string }> { + if (!enabled) { + return { + success: false, + error: "STUB: Voice integration is disabled. Set VOICE_ENABLED=true to enable.", + }; + } + + console.warn("Voice makeCall called but not implemented"); + return { + success: false, + error: "STUB: Voice integration not implemented", + }; + }, + + async transcribe(_audioUrl: string): Promise<{ success: boolean; text?: string; error?: string }> { + if (!enabled) { + return { + success: false, + error: "STUB: Voice integration is disabled", + }; + } + + return { + success: false, + error: "STUB: Voice transcription not implemented", + }; + }, + + async getStatus(): Promise<{ connected: boolean; error?: string }> { + return { + connected: false, + error: "STUB: Voice integration not implemented", + }; + }, + }; +} diff --git a/src/integrations/whatsapp/client.ts b/src/integrations/whatsapp/client.ts new file mode 100644 index 0000000..cdd9c08 --- /dev/null +++ b/src/integrations/whatsapp/client.ts @@ -0,0 +1,65 @@ +/** + * WhatsApp Integration - STUB + * This is a placeholder integration. Do not use in production without explicit approval. + */ + +export interface WhatsAppConfig { + apiUrl: string; + apiToken: string; +} + +export interface WhatsAppMessage { + to: string; + body: string; + type: "text" | "template"; +} + +export interface WhatsAppClient { + isEnabled(): boolean; + sendMessage(message: WhatsAppMessage): Promise<{ success: boolean; messageId?: string; error?: string }>; + getStatus(): Promise<{ connected: boolean; error?: string }>; +} + +/** + * Creates a WhatsApp client stub + * All methods return stub responses until properly configured + */ +export function createWhatsAppClient(_config?: WhatsAppConfig): WhatsAppClient { + const enabled = process.env.WHATSAPP_ENABLED === "true"; + + return { + isEnabled(): boolean { + return enabled; + }, + + async sendMessage(_message: WhatsAppMessage): Promise<{ success: boolean; messageId?: string; error?: string }> { + if (!enabled) { + return { + success: false, + error: "STUB: WhatsApp integration is disabled. Set WHATSAPP_ENABLED=true to enable.", + }; + } + + // TODO: Implement actual WhatsApp API call + console.warn("WhatsApp sendMessage called but not implemented"); + return { + success: false, + error: "STUB: WhatsApp integration not implemented", + }; + }, + + async getStatus(): Promise<{ connected: boolean; error?: string }> { + if (!enabled) { + return { + connected: false, + error: "STUB: WhatsApp integration is disabled", + }; + } + + return { + connected: false, + error: "STUB: WhatsApp integration not implemented", + }; + }, + }; +} diff --git a/src/meta/metaSupervisor.ts b/src/meta/metaSupervisor.ts new file mode 100644 index 0000000..dadf5cc --- /dev/null +++ b/src/meta/metaSupervisor.ts @@ -0,0 +1,165 @@ +/** + * Meta Supervisor Module + * Routes tasks across systems and aggregates health/metrics + */ + +export interface RouteDecision { + targetSystem: string; + reason: string; + constraints: string[]; + monitoringNotes: string[]; +} + +export interface SystemHealth { + systemId: string; + status: "healthy" | "degraded" | "unhealthy"; + taskCompletionRate: number; + stopRate: number; + avgStopScore: number; + queueDepth: number; + lastUpdated: string; +} + +export interface AggregatedMetrics { + totalTasks: number; + completedTasks: number; + stoppedTasks: number; + avgStopScore: number; + systemHealth: Record; +} + +/** + * Meta Supervisor + * Routes tasks and monitors system health + */ +export class MetaSupervisor { + private systems: Map = new Map(); + + constructor() { + // Register default system + this.registerSystem("code-cloud-agents"); + } + + /** + * Register a system for routing + */ + registerSystem(systemId: string): void { + this.systems.set(systemId, { + systemId, + status: "healthy", + taskCompletionRate: 1.0, + stopRate: 0, + avgStopScore: 0, + queueDepth: 0, + lastUpdated: new Date().toISOString(), + }); + } + + /** + * Route a task to the appropriate system + */ + route(taskType: string, constraints: string[] = []): RouteDecision { + // For now, route everything to the main system + const targetSystem = "code-cloud-agents"; + const systemHealth = this.systems.get(targetSystem); + + const monitoringNotes: string[] = []; + if (systemHealth) { + if (systemHealth.status !== "healthy") { + monitoringNotes.push(`System ${targetSystem} is ${systemHealth.status}`); + } + if (systemHealth.queueDepth > 10) { + monitoringNotes.push(`High queue depth: ${systemHealth.queueDepth}`); + } + if (systemHealth.stopRate > 0.3) { + monitoringNotes.push(`High stop rate: ${(systemHealth.stopRate * 100).toFixed(1)}%`); + } + } + + return { + targetSystem, + reason: `Task type "${taskType}" routed to primary system`, + constraints, + monitoringNotes, + }; + } + + /** + * Update system health metrics + */ + updateHealth(systemId: string, updates: Partial): void { + const current = this.systems.get(systemId); + if (!current) { + throw new Error(`System ${systemId} not registered`); + } + + this.systems.set(systemId, { + ...current, + ...updates, + lastUpdated: new Date().toISOString(), + }); + } + + /** + * Get aggregated metrics across all systems + */ + getAggregatedMetrics(): AggregatedMetrics { + let totalTasks = 0; + let completedTasks = 0; + let stoppedTasks = 0; + let totalStopScore = 0; + let scoreCount = 0; + + const systemHealth: Record = {}; + + for (const [id, health] of this.systems) { + systemHealth[id] = health; + + // Aggregate metrics (placeholder logic) + if (health.avgStopScore > 0) { + totalStopScore += health.avgStopScore; + scoreCount++; + } + } + + return { + totalTasks, + completedTasks, + stoppedTasks, + avgStopScore: scoreCount > 0 ? totalStopScore / scoreCount : 0, + systemHealth, + }; + } + + /** + * Check if any system requires attention + */ + checkAlerts(): string[] { + const alerts: string[] = []; + + for (const [id, health] of this.systems) { + if (health.status === "unhealthy") { + alerts.push(`CRITICAL: System ${id} is unhealthy`); + } else if (health.status === "degraded") { + alerts.push(`WARNING: System ${id} is degraded`); + } + + if (health.stopRate > 0.5) { + alerts.push(`HIGH STOP RATE: System ${id} has ${(health.stopRate * 100).toFixed(1)}% stop rate`); + } + + if (health.queueDepth > 50) { + alerts.push(`QUEUE OVERLOAD: System ${id} has ${health.queueDepth} pending tasks`); + } + } + + return alerts; + } +} + +/** + * Create a Meta Supervisor instance + */ +export function createMetaSupervisor(): MetaSupervisor { + return new MetaSupervisor(); +} diff --git a/src/queue/queue.ts b/src/queue/queue.ts new file mode 100644 index 0000000..264adc6 --- /dev/null +++ b/src/queue/queue.ts @@ -0,0 +1,118 @@ +/** + * Queue Module - In-memory queue with optional Redis/BullMQ support + */ + +export interface QueueJob { + id: string; + name: string; + data: Record; + status: "pending" | "processing" | "completed" | "failed"; + created_at: Date; + processed_at?: Date; +} + +export interface QueueAdapter { + mode: "in-memory" | "redis"; + isHealthy(): boolean; + add(name: string, data: Record): Promise; + process(name: string, handler: (job: QueueJob) => Promise): void; + getJob(id: string): QueueJob | undefined; + getStats(): { pending: number; processing: number; completed: number; failed: number }; +} + +/** + * In-memory queue implementation (fallback when Redis is not available) + */ +function createInMemoryQueue(): QueueAdapter { + const jobs = new Map(); + const handlers = new Map Promise>(); + let jobCounter = 0; + + return { + mode: "in-memory", + + isHealthy(): boolean { + return true; + }, + + async add(name: string, data: Record): Promise { + const id = `job_${++jobCounter}`; + const job: QueueJob = { + id, + name, + data, + status: "pending", + created_at: new Date(), + }; + jobs.set(id, job); + + // Process immediately if handler exists + const handler = handlers.get(name); + if (handler) { + setImmediate(async () => { + job.status = "processing"; + try { + await handler(job); + job.status = "completed"; + job.processed_at = new Date(); + } catch (error) { + job.status = "failed"; + console.error(`Job ${id} failed:`, error); + } + }); + } + + return id; + }, + + process(name: string, handler: (job: QueueJob) => Promise): void { + handlers.set(name, handler); + }, + + getJob(id: string): QueueJob | undefined { + return jobs.get(id); + }, + + getStats(): { pending: number; processing: number; completed: number; failed: number } { + let pending = 0; + let processing = 0; + let completed = 0; + let failed = 0; + + for (const job of jobs.values()) { + switch (job.status) { + case "pending": + pending++; + break; + case "processing": + processing++; + break; + case "completed": + completed++; + break; + case "failed": + failed++; + break; + } + } + + return { pending, processing, completed, failed }; + }, + }; +} + +/** + * Initialize queue adapter + * Uses in-memory queue by default, can be extended to support Redis/BullMQ + */ +export function initQueue(): QueueAdapter { + const redisUrl = process.env.REDIS_URL; + const queueEnabled = process.env.QUEUE_ENABLED === "true"; + + if (redisUrl && queueEnabled) { + // TODO: Implement Redis/BullMQ adapter when needed + console.log("Redis URL provided but BullMQ not implemented. Using in-memory queue."); + } + + return createInMemoryQueue(); +} diff --git a/src/supervisor/engineeringLead.ts b/src/supervisor/engineeringLead.ts new file mode 100644 index 0000000..7d61a5d --- /dev/null +++ b/src/supervisor/engineeringLead.ts @@ -0,0 +1,158 @@ +/** + * Engineering Lead Supervisor Module + * Controls truth, completeness, and cross-layer consistency + */ + +import { computeStopScore, analyzeContent, type StopReason, type StopScoreResult } from "../audit/stopScorer.js"; + +export interface SupervisorDecision { + decision: "APPROVED" | "STOP_REQUIRED"; + finalStatus: "COMPLETE" | "COMPLETE_WITH_GAPS" | "STOP_REQUIRED"; + riskLevel: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL"; + stopScore: number; + verifiedArtefacts: string[]; + missingInvalidParts: string[]; + requiredNextAction: string; +} + +export interface WorkSubmission { + taskId: string; + description: string; + artefacts: string[]; + claims: string[]; + hasTests: boolean; + hasSchema: boolean; + hasDeployConfig: boolean; +} + +/** + * Engineering Lead Supervisor + * Reviews work submissions and makes approval decisions + */ +export class EngineeringLeadSupervisor { + private readonly stopThreshold: number; + + constructor(stopThreshold = 40) { + this.stopThreshold = stopThreshold; + } + + /** + * Reviews a work submission and returns a decision + */ + review(submission: WorkSubmission): SupervisorDecision { + const reasons: StopReason[] = []; + + // Check for missing artefacts + if (submission.artefacts.length === 0) { + reasons.push("UNPROVEN_CLAIM"); + } + + // Check for missing tests + if (!submission.hasTests) { + reasons.push("MISSING_TESTS"); + } + + // Check for missing schema + if (!submission.hasSchema) { + reasons.push("MISSING_SQL_OR_SCHEMA"); + } + + // Check for missing deploy config + if (!submission.hasDeployConfig) { + reasons.push("MISSING_DEPLOY_CONFIG"); + } + + // Analyze description for problematic content + const contentReasons = analyzeContent(submission.description); + reasons.push(...contentReasons); + + // Analyze claims + for (const claim of submission.claims) { + const claimReasons = analyzeContent(claim); + reasons.push(...claimReasons); + } + + // Compute stop score + const scoreResult = computeStopScore([...new Set(reasons)]); + + // Make decision + const decision = this.makeDecision(scoreResult, submission); + + return decision; + } + + private makeDecision(scoreResult: StopScoreResult, submission: WorkSubmission): SupervisorDecision { + const stopRequired = scoreResult.stopRequired; + + let finalStatus: "COMPLETE" | "COMPLETE_WITH_GAPS" | "STOP_REQUIRED"; + if (stopRequired) { + finalStatus = "STOP_REQUIRED"; + } else if (scoreResult.reasons.length > 0) { + finalStatus = "COMPLETE_WITH_GAPS"; + } else { + finalStatus = "COMPLETE"; + } + + const missingParts: string[] = []; + if (!submission.hasTests) missingParts.push("Tests"); + if (!submission.hasSchema) missingParts.push("Schema/SQL"); + if (!submission.hasDeployConfig) missingParts.push("Deploy config"); + if (submission.artefacts.length === 0) missingParts.push("Verifiable artefacts"); + + let nextAction: string; + if (stopRequired) { + nextAction = `STOP: Address ${scoreResult.reasons.join(", ")} before proceeding`; + } else if (missingParts.length > 0) { + nextAction = `Add missing: ${missingParts.join(", ")}`; + } else { + nextAction = "Proceed to next task"; + } + + return { + decision: stopRequired ? "STOP_REQUIRED" : "APPROVED", + finalStatus, + riskLevel: scoreResult.severity, + stopScore: scoreResult.score, + verifiedArtefacts: submission.artefacts, + missingInvalidParts: missingParts, + requiredNextAction: nextAction, + }; + } + + /** + * Quick validation for common issues + */ + quickValidate(content: string): { valid: boolean; issues: string[] } { + const issues: string[] = []; + + if (content.includes("$") || content.includes("€")) { + if (!content.toLowerCase().includes("unknown")) { + issues.push("Pricing mentioned without UNKNOWN marker"); + } + } + + if (content.toLowerCase().includes("legal") || content.toLowerCase().includes("warranty")) { + issues.push("Legal statement detected"); + } + + const claimWords = ["implemented", "deployed", "connected", "scraped"]; + for (const word of claimWords) { + if (content.toLowerCase().includes(word) && !content.includes("file:")) { + issues.push(`Claim "${word}" without file reference`); + } + } + + return { + valid: issues.length === 0, + issues, + }; + } +} + +/** + * Create a default Engineering Lead Supervisor instance + */ +export function createEngineeringLeadSupervisor(): EngineeringLeadSupervisor { + const threshold = parseInt(process.env.STOP_SCORE_THRESHOLD ?? "40", 10); + return new EngineeringLeadSupervisor(threshold); +} diff --git a/tests/stopScorer.test.ts b/tests/stopScorer.test.ts new file mode 100644 index 0000000..f7c69b6 --- /dev/null +++ b/tests/stopScorer.test.ts @@ -0,0 +1,81 @@ +/** + * Tests for STOP Scorer + */ + +import { test } from "node:test"; +import assert from "node:assert"; +import { computeStopScore, analyzeContent, type StopReason } from "../src/audit/stopScorer.js"; + +test("computeStopScore returns LOW severity for empty reasons", () => { + const result = computeStopScore([]); + + assert.strictEqual(result.score, 0); + assert.strictEqual(result.severity, "LOW"); + assert.strictEqual(result.stopRequired, false); +}); + +test("computeStopScore returns HIGH severity for pricing violation", () => { + const reasons: StopReason[] = ["PRICING_WITHOUT_FACT"]; + const result = computeStopScore(reasons); + + assert.strictEqual(result.score, 40); + assert.strictEqual(result.severity, "HIGH"); + assert.strictEqual(result.stopRequired, true); +}); + +test("computeStopScore returns CRITICAL severity for multiple violations", () => { + const reasons: StopReason[] = [ + "PRICING_WITHOUT_FACT", + "LEGAL_STATEMENT", + "UNPROVEN_CLAIM", + ]; + const result = computeStopScore(reasons); + + assert.strictEqual(result.score, 100); // 40 + 40 + 30 = 110, capped at 100 + assert.strictEqual(result.severity, "CRITICAL"); + assert.strictEqual(result.stopRequired, true); +}); + +test("computeStopScore returns MEDIUM severity for missing tests only", () => { + const reasons: StopReason[] = ["MISSING_TESTS", "MISSING_DEPLOY_CONFIG"]; + const result = computeStopScore(reasons); + + assert.strictEqual(result.score, 30); // 15 + 15 + assert.strictEqual(result.severity, "MEDIUM"); + assert.strictEqual(result.stopRequired, false); +}); + +test("analyzeContent detects pricing without UNKNOWN marker", () => { + const content = "Set the price to $99/month"; + const reasons = analyzeContent(content); + + assert.ok(reasons.includes("PRICING_WITHOUT_FACT")); +}); + +test("analyzeContent does not flag pricing with UNKNOWN marker", () => { + const content = "Price is UNKNOWN - requires approval"; + const reasons = analyzeContent(content); + + assert.ok(!reasons.includes("PRICING_WITHOUT_FACT")); +}); + +test("analyzeContent detects legal statements", () => { + const content = "Added legal disclaimer to the footer"; + const reasons = analyzeContent(content); + + assert.ok(reasons.includes("LEGAL_STATEMENT")); +}); + +test("analyzeContent detects unproven claims", () => { + const content = "Successfully implemented the feature"; + const reasons = analyzeContent(content); + + assert.ok(reasons.includes("UNPROVEN_CLAIM")); +}); + +test("analyzeContent does not flag claims with file references", () => { + const content = "Implemented in file: src/feature.ts"; + const reasons = analyzeContent(content); + + assert.ok(!reasons.includes("UNPROVEN_CLAIM")); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..79e903a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "tests"] +} From 16af29503f009b18aa5034ee715aef5bcddbc415 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Dec 2025 21:09:48 +0000 Subject: [PATCH 3/4] Add Hard STOP enforcement with human approval flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements ChatGPT's recommended improvements: 1. HARD GATE ENFORCEMENT - EnforcementGate class blocks tasks when STOP_REQUIRED - No task proceeds without explicit human approval - All decisions are audited 2. HUMAN APPROVAL FLOW - POST /api/enforcement/approve/:taskId - requires: * approver identity * reason (min 10 chars) * acknowledged_risks array - POST /api/enforcement/reject/:taskId - permanent stop - GET /api/enforcement/blocked - list blocked tasks 3. CLAIM-ARTEFACT VERIFICATION - Detects lies: "scraped 10 URLs" but only 1 file → STOP - Pattern matching for common claims - Generates verification reports 4. API INTEGRATION - Task creation evaluates content through gate - Work submission triggers enforcement check - Task GET shows enforcement status Key difference from previous version: - STOP is now BLOCKING, not just a status - No automation can override STOP - Human must explicitly approve with accountability --- src/api/enforcement.ts | 166 ++++++++++++++++++++++ src/api/tasks.ts | 103 +++++++++++++- src/audit/claimVerifier.ts | 200 ++++++++++++++++++++++++++ src/audit/enforcementGate.ts | 254 ++++++++++++++++++++++++++++++++++ src/index.ts | 22 ++- tests/enforcementGate.test.ts | 78 +++++++++++ 6 files changed, 813 insertions(+), 10 deletions(-) create mode 100644 src/api/enforcement.ts create mode 100644 src/audit/claimVerifier.ts create mode 100644 src/audit/enforcementGate.ts create mode 100644 tests/enforcementGate.test.ts diff --git a/src/api/enforcement.ts b/src/api/enforcement.ts new file mode 100644 index 0000000..2a29d09 --- /dev/null +++ b/src/api/enforcement.ts @@ -0,0 +1,166 @@ +/** + * Enforcement Router - Human approval endpoints for blocked tasks + * + * These endpoints require human intervention. + * No automation can approve a STOP_REQUIRED task. + */ + +import { Router } from "express"; +import { z } from "zod"; +import type { EnforcementGate } from "../audit/enforcementGate.js"; +import { verifyClaims, generateVerificationReport } from "../audit/claimVerifier.js"; + +const ApproveSchema = z.object({ + approver: z.string().min(1, "Approver identity required"), + reason: z.string().min(10, "Reason must be at least 10 characters"), + acknowledged_risks: z.array(z.string()).min(1, "Must acknowledge at least one risk"), +}); + +const RejectSchema = z.object({ + rejector: z.string().min(1, "Rejector identity required"), + reason: z.string().min(10, "Reason must be at least 10 characters"), +}); + +const VerifyClaimsSchema = z.object({ + content: z.string().min(1), + artefacts: z.array(z.string()), +}); + +export function createEnforcementRouter(gate: EnforcementGate): Router { + const router = Router(); + + /** + * GET /enforcement/blocked + * List all currently blocked tasks awaiting human approval + */ + router.get("/blocked", (_req, res) => { + const pending = gate.getAllPendingApprovals(); + res.json({ + blocked_count: pending.length, + tasks: pending, + message: pending.length > 0 + ? "⚠️ Tasks blocked. Human approval required." + : "✅ No blocked tasks.", + }); + }); + + /** + * GET /enforcement/blocked/:taskId + * Get details of a specific blocked task + */ + router.get("/blocked/:taskId", (req, res) => { + const pending = gate.getPendingApproval(req.params.taskId); + + if (!pending) { + return res.status(404).json({ + error: "Task not found or not blocked", + }); + } + + res.json({ + ...pending, + message: "⚠️ This task is BLOCKED. Human approval required to proceed.", + required_fields: { + approver: "Your identity (email/name)", + reason: "Why you are approving despite STOP (min 10 chars)", + acknowledged_risks: "List of risks you are accepting", + }, + }); + }); + + /** + * POST /enforcement/approve/:taskId + * Approve a blocked task - REQUIRES human action + */ + router.post("/approve/:taskId", (req, res) => { + try { + const parsed = ApproveSchema.safeParse(req.body); + + if (!parsed.success) { + return res.status(400).json({ + error: "Invalid approval request", + details: parsed.error.issues, + required_fields: { + approver: "string (required)", + reason: "string min 10 chars (required)", + acknowledged_risks: "string[] min 1 item (required)", + }, + }); + } + + const decision = gate.approve( + req.params.taskId, + parsed.data.approver, + parsed.data.reason, + parsed.data.acknowledged_risks + ); + + res.json({ + status: "APPROVED", + decision, + warning: "⚠️ Task approved despite STOP condition. This has been logged.", + }); + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + res.status(400).json({ error: message }); + } + }); + + /** + * POST /enforcement/reject/:taskId + * Reject a blocked task permanently + */ + router.post("/reject/:taskId", (req, res) => { + try { + const parsed = RejectSchema.safeParse(req.body); + + if (!parsed.success) { + return res.status(400).json({ + error: "Invalid rejection request", + details: parsed.error.issues, + }); + } + + gate.reject(req.params.taskId, parsed.data.rejector, parsed.data.reason); + + res.json({ + status: "REJECTED", + message: "Task has been permanently stopped and logged.", + }); + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + res.status(400).json({ error: message }); + } + }); + + /** + * POST /enforcement/verify-claims + * Verify claims against artefacts (for testing/validation) + */ + router.post("/verify-claims", (req, res) => { + try { + const parsed = VerifyClaimsSchema.safeParse(req.body); + + if (!parsed.success) { + return res.status(400).json({ + error: "Invalid verification request", + details: parsed.error.issues, + }); + } + + const result = verifyClaims(parsed.data.content, parsed.data.artefacts); + const report = generateVerificationReport(result); + + res.json({ + ...result, + report, + status: result.allVerified ? "VERIFIED" : "STOP_REQUIRED", + }); + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + res.status(500).json({ error: message }); + } + }); + + return router; +} diff --git a/src/api/tasks.ts b/src/api/tasks.ts index 4355d4f..ac24cad 100644 --- a/src/api/tasks.ts +++ b/src/api/tasks.ts @@ -1,20 +1,31 @@ /** * Tasks Router - Create and manage supervised tasks + * + * ENFORCEMENT: All tasks are evaluated by the EnforcementGate. + * If STOP_REQUIRED → task is BLOCKED until human approval. */ import { Router } from "express"; import { z } from "zod"; import type { Database } from "../db/database.js"; import type { QueueAdapter } from "../queue/queue.js"; +import type { EnforcementGate } from "../audit/enforcementGate.js"; const CreateTaskSchema = z.object({ title: z.string().min(1).max(200), description: z.string().optional(), priority: z.enum(["low", "medium", "high"]).default("medium"), assignee: z.enum(["cloud_assistant"]).default("cloud_assistant"), + artefacts: z.array(z.string()).optional().default([]), }); -export function createTaskRouter(db: Database, queue: QueueAdapter): Router { +const SubmitWorkSchema = z.object({ + content: z.string().min(1), + artefacts: z.array(z.string()).default([]), + claims: z.array(z.string()).optional().default([]), +}); + +export function createTaskRouter(db: Database, queue: QueueAdapter, gate: EnforcementGate): Router { const router = Router(); // Create task @@ -30,17 +41,39 @@ export function createTaskRouter(db: Database, queue: QueueAdapter): Router { } const task = db.createTask({ - ...parsed.data, + title: parsed.data.title, + description: parsed.data.description, + priority: parsed.data.priority, + assignee: parsed.data.assignee, status: "pending", created_at: new Date().toISOString(), }); - // Add to queue + // ENFORCEMENT GATE: Evaluate task content before queuing + const content = `${parsed.data.title} ${parsed.data.description ?? ""}`; + const gateDecision = gate.evaluate(task.id, content, parsed.data.artefacts); + + if (gateDecision.status === "BLOCKED") { + // Task is BLOCKED - do NOT queue, require human approval + db.updateTask(task.id, { status: "stopped", stop_score: gateDecision.stopScore }); + + return res.status(202).json({ + id: task.id, + status: "BLOCKED", + stop_score: gateDecision.stopScore, + reasons: gateDecision.reasons, + message: "⚠️ STOP_REQUIRED: Task blocked. Human approval needed at /api/enforcement/approve/" + task.id, + approval_url: `/api/enforcement/approve/${task.id}`, + }); + } + + // Task passed gate - queue for processing await queue.add("process_task", { taskId: task.id }); res.status(201).json({ id: task.id, status: "pending", + gate_status: "OPEN", message: "Task created and queued for processing", }); } catch (error) { @@ -49,6 +82,57 @@ export function createTaskRouter(db: Database, queue: QueueAdapter): Router { } }); + // Submit work for a task (triggers enforcement gate) + router.post("/:id/submit", async (req, res) => { + try { + const task = db.getTask(req.params.id); + + if (!task) { + return res.status(404).json({ error: "Task not found" }); + } + + const parsed = SubmitWorkSchema.safeParse(req.body); + + if (!parsed.success) { + return res.status(400).json({ + error: "Invalid submission", + details: parsed.error.issues, + }); + } + + // ENFORCEMENT GATE: Evaluate submitted work + const gateDecision = gate.evaluate(task.id, parsed.data.content, parsed.data.artefacts); + + if (gateDecision.status === "BLOCKED") { + // Work is BLOCKED - cannot complete task + db.updateTask(task.id, { status: "stopped", stop_score: gateDecision.stopScore }); + + return res.status(202).json({ + id: task.id, + status: "BLOCKED", + stop_score: gateDecision.stopScore, + reasons: gateDecision.reasons, + message: "⚠️ STOP_REQUIRED: Submission blocked. Human approval needed.", + approval_url: `/api/enforcement/approve/${task.id}`, + }); + } + + // Work passed gate - mark as completed + db.updateTask(task.id, { status: "completed", stop_score: gateDecision.stopScore }); + + res.json({ + id: task.id, + status: "completed", + gate_status: "OPEN", + stop_score: gateDecision.stopScore, + message: "✅ Work submitted and approved", + }); + } catch (error) { + console.error("Failed to submit work:", error); + res.status(500).json({ error: "Internal server error" }); + } + }); + // List tasks router.get("/", (_req, res) => { try { @@ -69,7 +153,18 @@ export function createTaskRouter(db: Database, queue: QueueAdapter): Router { return res.status(404).json({ error: "Task not found" }); } - res.json(task); + // Check if task is blocked + const isBlocked = gate.isBlocked(req.params.id); + const pending = gate.getPendingApproval(req.params.id); + + res.json({ + ...task, + enforcement: { + blocked: isBlocked, + pending_approval: pending ?? null, + approval_url: isBlocked ? `/api/enforcement/approve/${task.id}` : null, + }, + }); } catch (error) { console.error("Failed to get task:", error); res.status(500).json({ error: "Internal server error" }); diff --git a/src/audit/claimVerifier.ts b/src/audit/claimVerifier.ts new file mode 100644 index 0000000..2a9726a --- /dev/null +++ b/src/audit/claimVerifier.ts @@ -0,0 +1,200 @@ +/** + * Claim-Artefact Verifier + * + * Detects lies: when claims don't match artefacts. + * "10 URLs scraped" but only 1 file? → STOP. + */ + +import * as fs from "fs"; +import * as path from "path"; + +export interface ClaimVerification { + claim: string; + claimedCount: number; + actualCount: number; + verified: boolean; + discrepancy: number; + evidence: string[]; +} + +export interface VerificationResult { + allVerified: boolean; + totalClaims: number; + verifiedClaims: number; + failedClaims: ClaimVerification[]; + passedClaims: ClaimVerification[]; +} + +/** + * Claim patterns we look for + */ +const CLAIM_PATTERNS = [ + { pattern: /scraped\s+(\d+)\s*(urls?|pages?|sites?|portals?)/i, type: "scrape" }, + { pattern: /created\s+(\d+)\s*(files?|components?|modules?)/i, type: "file" }, + { pattern: /implemented\s+(\d+)\s*(features?|endpoints?|apis?)/i, type: "feature" }, + { pattern: /fixed\s+(\d+)\s*(bugs?|issues?|errors?)/i, type: "fix" }, + { pattern: /added\s+(\d+)\s*(tests?|specs?)/i, type: "test" }, + { pattern: /deployed\s+(\d+)\s*(services?|instances?)/i, type: "deploy" }, +]; + +/** + * Verify claims against artefacts + */ +export function verifyClaims( + content: string, + artefacts: string[], + projectRoot?: string +): VerificationResult { + const verifications: ClaimVerification[] = []; + + for (const { pattern, type } of CLAIM_PATTERNS) { + const matches = content.matchAll(new RegExp(pattern, "gi")); + + for (const match of matches) { + const claimedCount = parseInt(match[1], 10); + const claim = match[0]; + + // Count relevant artefacts + const relevantArtefacts = filterArtefactsByType(artefacts, type, projectRoot); + const actualCount = relevantArtefacts.length; + + const verification: ClaimVerification = { + claim, + claimedCount, + actualCount, + verified: actualCount >= claimedCount, + discrepancy: claimedCount - actualCount, + evidence: relevantArtefacts, + }; + + verifications.push(verification); + } + } + + const failedClaims = verifications.filter((v) => !v.verified); + const passedClaims = verifications.filter((v) => v.verified); + + return { + allVerified: failedClaims.length === 0, + totalClaims: verifications.length, + verifiedClaims: passedClaims.length, + failedClaims, + passedClaims, + }; +} + +/** + * Filter artefacts by claim type + */ +function filterArtefactsByType( + artefacts: string[], + type: string, + projectRoot?: string +): string[] { + const relevant: string[] = []; + + for (const artefact of artefacts) { + // Check if artefact exists (if projectRoot provided) + if (projectRoot) { + const fullPath = path.join(projectRoot, artefact); + if (!fs.existsSync(fullPath)) { + continue; // Skip non-existent artefacts + } + } + + switch (type) { + case "scrape": + // Look for data files, JSON, CSV + if (/\.(json|csv|xml|html|txt)$/i.test(artefact)) { + relevant.push(artefact); + } + break; + + case "file": + // Any file counts + relevant.push(artefact); + break; + + case "feature": + // Source files + if (/\.(ts|js|tsx|jsx|py|go|rs|java)$/i.test(artefact)) { + relevant.push(artefact); + } + break; + + case "fix": + // Modified source files + if (/\.(ts|js|tsx|jsx|py|go|rs|java)$/i.test(artefact)) { + relevant.push(artefact); + } + break; + + case "test": + // Test files + if (/\.(test|spec)\.(ts|js|tsx|jsx)$/i.test(artefact) || /tests?\//i.test(artefact)) { + relevant.push(artefact); + } + break; + + case "deploy": + // Deploy/config files + if (/\.(yaml|yml|json|toml|dockerfile)$/i.test(artefact) || /docker|k8s|deploy/i.test(artefact)) { + relevant.push(artefact); + } + break; + + default: + relevant.push(artefact); + } + } + + return relevant; +} + +/** + * Generate a verification report + */ +export function generateVerificationReport(result: VerificationResult): string { + const lines: string[] = []; + + lines.push("# Claim Verification Report"); + lines.push(""); + lines.push(`## Summary`); + lines.push(`- Total Claims: ${result.totalClaims}`); + lines.push(`- Verified: ${result.verifiedClaims}`); + lines.push(`- Failed: ${result.failedClaims.length}`); + lines.push(`- Status: ${result.allVerified ? "✅ ALL VERIFIED" : "❌ VERIFICATION FAILED"}`); + lines.push(""); + + if (result.failedClaims.length > 0) { + lines.push("## ❌ Failed Claims (STOP REQUIRED)"); + lines.push(""); + + for (const failed of result.failedClaims) { + lines.push(`### "${failed.claim}"`); + lines.push(`- Claimed: ${failed.claimedCount}`); + lines.push(`- Actual: ${failed.actualCount}`); + lines.push(`- Discrepancy: ${failed.discrepancy} missing`); + lines.push(`- Evidence found:`); + if (failed.evidence.length === 0) { + lines.push(" - (none)"); + } else { + for (const e of failed.evidence) { + lines.push(` - ${e}`); + } + } + lines.push(""); + } + } + + if (result.passedClaims.length > 0) { + lines.push("## ✅ Verified Claims"); + lines.push(""); + + for (const passed of result.passedClaims) { + lines.push(`- "${passed.claim}" → ${passed.actualCount} artefacts found`); + } + } + + return lines.join("\n"); +} diff --git a/src/audit/enforcementGate.ts b/src/audit/enforcementGate.ts new file mode 100644 index 0000000..1b682d7 --- /dev/null +++ b/src/audit/enforcementGate.ts @@ -0,0 +1,254 @@ +/** + * Enforcement Gate - Hard STOP enforcement + * + * This module ensures that STOP_REQUIRED decisions are BLOCKING. + * No task proceeds without explicit human approval after STOP. + */ + +import type { Database } from "../db/database.js"; +import { computeStopScore, analyzeContent, type StopReason } from "../audit/stopScorer.js"; + +export type GateStatus = "OPEN" | "BLOCKED" | "AWAITING_APPROVAL"; + +export interface GateDecision { + status: GateStatus; + taskId: string; + stopScore: number; + reasons: StopReason[]; + blockedAt?: string; + approvedBy?: string; + approvedAt?: string; +} + +export interface PendingApproval { + taskId: string; + stopScore: number; + reasons: StopReason[]; + blockedAt: string; + requiredAction: string; +} + +/** + * Enforcement Gate + * + * HARD RULES: + * 1. If STOP_REQUIRED → task is BLOCKED, period. + * 2. No continuation without human approval. + * 3. Approval requires explicit action + reason. + * 4. All decisions are audited. + */ +export class EnforcementGate { + private blockedTasks: Map = new Map(); + private db: Database; + + constructor(db: Database) { + this.db = db; + } + + /** + * Evaluate content and determine if it can proceed + * Returns BLOCKED if any STOP condition is met + */ + evaluate(taskId: string, content: string, artefacts: string[]): GateDecision { + const reasons: StopReason[] = []; + + // Analyze content for violations + const contentReasons = analyzeContent(content); + reasons.push(...contentReasons); + + // Check for unproven claims (claims without artefacts) + if (this.hasUnprovenClaims(content, artefacts)) { + reasons.push("UNPROVEN_CLAIM"); + } + + // Compute score + const scoreResult = computeStopScore([...new Set(reasons)]); + + // HARD GATE: If stop required, BLOCK immediately + if (scoreResult.stopRequired) { + const pending: PendingApproval = { + taskId, + stopScore: scoreResult.score, + reasons: scoreResult.reasons, + blockedAt: new Date().toISOString(), + requiredAction: `Address: ${scoreResult.reasons.join(", ")}`, + }; + + this.blockedTasks.set(taskId, pending); + + // Log to audit + this.db.createAuditEntry({ + task_id: taskId, + decision: "STOP_REQUIRED", + final_status: "STOP_REQUIRED", + risk_level: scoreResult.severity, + stop_score: scoreResult.score, + verified_artefacts: JSON.stringify(artefacts), + missing_invalid_parts: JSON.stringify(scoreResult.reasons), + required_next_action: "BLOCKED - Awaiting human approval", + }); + + return { + status: "BLOCKED", + taskId, + stopScore: scoreResult.score, + reasons: scoreResult.reasons, + blockedAt: pending.blockedAt, + }; + } + + // OPEN - can proceed + return { + status: "OPEN", + taskId, + stopScore: scoreResult.score, + reasons: scoreResult.reasons, + }; + } + + /** + * Check if task is blocked + */ + isBlocked(taskId: string): boolean { + return this.blockedTasks.has(taskId); + } + + /** + * Get pending approval for a task + */ + getPendingApproval(taskId: string): PendingApproval | undefined { + return this.blockedTasks.get(taskId); + } + + /** + * Get all pending approvals + */ + getAllPendingApprovals(): PendingApproval[] { + return Array.from(this.blockedTasks.values()); + } + + /** + * Approve a blocked task - REQUIRES explicit human action + * + * @param taskId - The blocked task + * @param approver - Human identifier (email, name, etc.) + * @param reason - Why this is being approved despite STOP + * @param acknowledgedRisks - Must explicitly list acknowledged risks + */ + approve( + taskId: string, + approver: string, + reason: string, + acknowledgedRisks: string[] + ): GateDecision { + const pending = this.blockedTasks.get(taskId); + + if (!pending) { + throw new Error(`Task ${taskId} is not blocked or does not exist`); + } + + if (!approver || approver.trim().length === 0) { + throw new Error("Approver identity is required"); + } + + if (!reason || reason.trim().length < 10) { + throw new Error("Approval reason must be at least 10 characters"); + } + + if (acknowledgedRisks.length === 0) { + throw new Error("Must explicitly acknowledge at least one risk"); + } + + // Log the override to audit + this.db.createAuditEntry({ + task_id: taskId, + decision: "APPROVED", + final_status: "COMPLETE_WITH_GAPS", + risk_level: pending.stopScore >= 70 ? "CRITICAL" : "HIGH", + stop_score: pending.stopScore, + verified_artefacts: JSON.stringify([]), + missing_invalid_parts: JSON.stringify({ + original_reasons: pending.reasons, + override_reason: reason, + acknowledged_risks: acknowledgedRisks, + }), + required_next_action: `OVERRIDE by ${approver}: ${reason}`, + }); + + // Remove from blocked + this.blockedTasks.delete(taskId); + + return { + status: "OPEN", + taskId, + stopScore: pending.stopScore, + reasons: pending.reasons, + approvedBy: approver, + approvedAt: new Date().toISOString(), + }; + } + + /** + * Reject and keep blocked - explicit denial + */ + reject(taskId: string, rejector: string, reason: string): void { + const pending = this.blockedTasks.get(taskId); + + if (!pending) { + throw new Error(`Task ${taskId} is not blocked`); + } + + // Update the task status to permanently stopped + this.db.updateTask(taskId, { + status: "stopped", + stop_score: pending.stopScore, + }); + + // Log rejection + this.db.createAuditEntry({ + task_id: taskId, + decision: "STOP_REQUIRED", + final_status: "STOP_REQUIRED", + risk_level: "CRITICAL", + stop_score: pending.stopScore, + verified_artefacts: JSON.stringify([]), + missing_invalid_parts: JSON.stringify(pending.reasons), + required_next_action: `REJECTED by ${rejector}: ${reason}`, + }); + + this.blockedTasks.delete(taskId); + } + + /** + * Check for unproven claims + */ + private hasUnprovenClaims(content: string, artefacts: string[]): boolean { + const claimPatterns = [ + /implemented\s+(\d+)/i, + /created\s+(\d+)/i, + /scraped\s+(\d+)/i, + /deployed\s+(\d+)/i, + /connected\s+(\d+)/i, + ]; + + for (const pattern of claimPatterns) { + const match = content.match(pattern); + if (match) { + const claimedCount = parseInt(match[1], 10); + // If claiming N things but not providing N artefacts, it's unproven + if (artefacts.length < claimedCount) { + return true; + } + } + } + + return false; + } +} + +/** + * Create enforcement gate instance + */ +export function createEnforcementGate(db: Database): EnforcementGate { + return new EnforcementGate(db); +} diff --git a/src/index.ts b/src/index.ts index a9e1667..bfee60c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,8 +7,10 @@ import express from "express"; import { createHealthRouter } from "./api/health.js"; import { createTaskRouter } from "./api/tasks.js"; import { createAuditRouter } from "./api/audit.js"; +import { createEnforcementRouter } from "./api/enforcement.js"; import { initDatabase } from "./db/database.js"; import { initQueue } from "./queue/queue.js"; +import { createEnforcementGate } from "./audit/enforcementGate.js"; const PORT = process.env.PORT ?? 3000; @@ -23,14 +25,19 @@ async function main() { const queue = initQueue(); console.log("✅ Queue initialized (mode:", queue.mode, ")"); + // Initialize enforcement gate (HARD STOP enforcement) + const gate = createEnforcementGate(db); + console.log("✅ Enforcement Gate active (STOP decisions are BLOCKING)"); + // Create Express app const app = express(); app.use(express.json()); // Mount routers app.use("/health", createHealthRouter(db, queue)); - app.use("/api/tasks", createTaskRouter(db, queue)); + app.use("/api/tasks", createTaskRouter(db, queue, gate)); app.use("/api/audit", createAuditRouter(db)); + app.use("/api/enforcement", createEnforcementRouter(gate)); // Root endpoint app.get("/", (_req, res) => { @@ -47,11 +54,14 @@ async function main() { app.listen(PORT, () => { console.log(`✅ Server running on http://localhost:${PORT}`); console.log("📋 Endpoints:"); - console.log(" GET / - Service info"); - console.log(" GET /health - Health check"); - console.log(" POST /api/tasks - Create task"); - console.log(" GET /api/tasks - List tasks"); - console.log(" GET /api/audit - Audit log"); + console.log(" GET / - Service info"); + console.log(" GET /health - Health check"); + console.log(" POST /api/tasks - Create task"); + console.log(" GET /api/tasks - List tasks"); + console.log(" GET /api/audit - Audit log"); + console.log(" GET /api/enforcement/blocked - Blocked tasks (STOP)"); + console.log(" POST /api/enforcement/approve - Human approval"); + console.log(" POST /api/enforcement/reject - Human rejection"); }); } diff --git a/tests/enforcementGate.test.ts b/tests/enforcementGate.test.ts new file mode 100644 index 0000000..35e8c22 --- /dev/null +++ b/tests/enforcementGate.test.ts @@ -0,0 +1,78 @@ +/** + * Tests for Enforcement Gate - Hard STOP enforcement + */ + +import { test } from "node:test"; +import assert from "node:assert"; +import { verifyClaims, generateVerificationReport } from "../src/audit/claimVerifier.js"; + +test("verifyClaims detects unproven scraping claims", () => { + const content = "Successfully scraped 10 portals and stored the data"; + const artefacts = ["data/portal1.json"]; // Only 1 artefact for 10 claimed + + const result = verifyClaims(content, artefacts); + + assert.strictEqual(result.allVerified, false); + assert.strictEqual(result.failedClaims.length, 1); + assert.strictEqual(result.failedClaims[0].claimedCount, 10); + assert.strictEqual(result.failedClaims[0].actualCount, 1); + assert.strictEqual(result.failedClaims[0].discrepancy, 9); +}); + +test("verifyClaims passes when artefacts match claims", () => { + const content = "Created 3 files for the feature"; + const artefacts = ["src/feature.ts", "src/types.ts", "src/utils.ts"]; + + const result = verifyClaims(content, artefacts); + + assert.strictEqual(result.allVerified, true); + assert.strictEqual(result.failedClaims.length, 0); + assert.strictEqual(result.passedClaims.length, 1); +}); + +test("verifyClaims handles no claims in content", () => { + const content = "Fixed a bug in the login flow"; + const artefacts = ["src/login.ts"]; + + const result = verifyClaims(content, artefacts); + + assert.strictEqual(result.allVerified, true); + assert.strictEqual(result.totalClaims, 0); +}); + +test("verifyClaims detects multiple unproven claims", () => { + const content = "Implemented 5 features and added 10 tests"; + const artefacts = ["src/feature1.ts", "tests/test1.test.ts"]; + + const result = verifyClaims(content, artefacts); + + assert.strictEqual(result.allVerified, false); + assert.strictEqual(result.failedClaims.length, 2); +}); + +test("generateVerificationReport creates readable output", () => { + const content = "Scraped 5 URLs"; + const artefacts = ["data/url1.json"]; + + const result = verifyClaims(content, artefacts); + const report = generateVerificationReport(result); + + assert.ok(report.includes("Claim Verification Report")); + assert.ok(report.includes("VERIFICATION FAILED")); + assert.ok(report.includes("Claimed: 5")); + assert.ok(report.includes("Actual: 1")); +}); + +test("verifyClaims filters artefacts by type correctly", () => { + const content = "Added 2 tests"; + const artefacts = [ + "src/feature.ts", // Not a test file + "tests/feature.test.ts", // Test file + "tests/utils.test.ts", // Test file + ]; + + const result = verifyClaims(content, artefacts); + + assert.strictEqual(result.allVerified, true); + assert.strictEqual(result.passedClaims[0].actualCount, 2); +}); From 6d0c0202f2ca92d400fa922ea7b26b9a873c771f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Dec 2025 21:31:33 +0000 Subject: [PATCH 4/4] Add Supervisor Dashboard with full control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Features: - Dashboard overview with live stats - Task creation form - Blocked tasks management (approve/reject) - Audit log viewer - Settings page with STOP score thresholds - Auto-refresh every 5 seconds - Toast notifications - Dark theme (GitHub-style) Endpoints now serve: - / → Dashboard (HTML) - /api → API info (JSON) - /health → Health check - /api/* → All API endpoints --- public/index.html | 825 ++++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 22 +- 2 files changed, 841 insertions(+), 6 deletions(-) create mode 100644 public/index.html diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..e906f7f --- /dev/null +++ b/public/index.html @@ -0,0 +1,825 @@ + + + + + + Code Cloud Agents - Supervisor Dashboard + + + + + +
+ +
+ + +
+
+
-
+
System Status
+
+
+
0
+
Blocked Tasks
+
+
+
0
+
Total Tasks
+
+
+
0
+
Completed
+
+
+
0
+
Avg Stop Score
+
+
+ +
+
+
+

⚠️ Awaiting Approval

+
+
+
+ +
+
+

📜 Recent Audit

+
+
+
+
+
+ + +
+ + +
+
+
+
+ + +
+ + +
+
+
+
+ + +
+ + +
+
+
0
+
Total Decisions
+
+
+
0
+
Stopped
+
+
+
0
+
Approved
+
+
+ +
+
+
+
+ + +
+ + +
+
+

System Info

+
+
+

Version: 0.1.0

+

Supervisor: ENGINEERING_LEAD_SUPERVISOR

+

Mode: SUPERVISED

+

Stop Threshold: 40

+
+
+ +
+
+

STOP Score Schwellenwerte

+
+
+ + + + + + + + + +
PRICING_WITHOUT_FACT+40
LEGAL_STATEMENT+40
UNPROVEN_CLAIM+30
CROSS_LAYER_MISMATCH+25
MISSING_SQL_OR_SCHEMA+25
COST_OR_LOAD_RISK+20
MISSING_TESTS+15
MISSING_DEPLOY_CONFIG+15
+

+ Score >= 40 = STOP_REQUIRED (Task wird blockiert) +

+
+
+
+
+ + + + + + + + + + + +
+ +
+ + + + diff --git a/src/index.ts b/src/index.ts index bfee60c..109784b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,8 @@ */ import express from "express"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; import { createHealthRouter } from "./api/health.js"; import { createTaskRouter } from "./api/tasks.js"; import { createAuditRouter } from "./api/audit.js"; @@ -12,6 +14,9 @@ import { initDatabase } from "./db/database.js"; import { initQueue } from "./queue/queue.js"; import { createEnforcementGate } from "./audit/enforcementGate.js"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + const PORT = process.env.PORT ?? 3000; async function main() { @@ -33,14 +38,18 @@ async function main() { const app = express(); app.use(express.json()); - // Mount routers + // Serve static dashboard + const publicPath = join(__dirname, "..", "public"); + app.use(express.static(publicPath)); + + // Mount API routers app.use("/health", createHealthRouter(db, queue)); app.use("/api/tasks", createTaskRouter(db, queue, gate)); app.use("/api/audit", createAuditRouter(db)); app.use("/api/enforcement", createEnforcementRouter(gate)); - // Root endpoint - app.get("/", (_req, res) => { + // API info endpoint + app.get("/api", (_req, res) => { res.json({ name: "code-cloud-agents", version: "0.1.0", @@ -53,13 +62,14 @@ async function main() { // Start server app.listen(PORT, () => { console.log(`✅ Server running on http://localhost:${PORT}`); - console.log("📋 Endpoints:"); - console.log(" GET / - Service info"); + console.log("📋 Dashboard: http://localhost:" + PORT); + console.log("📋 API Endpoints:"); + console.log(" GET /api - API info"); console.log(" GET /health - Health check"); console.log(" POST /api/tasks - Create task"); console.log(" GET /api/tasks - List tasks"); console.log(" GET /api/audit - Audit log"); - console.log(" GET /api/enforcement/blocked - Blocked tasks (STOP)"); + console.log(" GET /api/enforcement/blocked - Blocked tasks"); console.log(" POST /api/enforcement/approve - Human approval"); console.log(" POST /api/enforcement/reject - Human rejection"); });