diff --git a/docs/api-router-procedure-support-backlog.md b/docs/api-router-procedure-support-backlog.md index 0f1c502..8f7db8d 100644 --- a/docs/api-router-procedure-support-backlog.md +++ b/docs/api-router-procedure-support-backlog.md @@ -31,14 +31,14 @@ Done - `project` Ready next -- none in the conflict-safe backlog +- `resource-read-shared` Deferred or blocked - `assistant-tools` -- `resource-read-shared` Notes - Router files should contain only tRPC wiring. - Procedure orchestration and router input schemas belong in `*-procedure-support.ts`. - Pure builders and domain helpers remain in existing `*-support.ts` modules. - Targeted router verification passed on 2026-03-31 for `timeline-router` and `vacation-router`. +- Der naechste konfliktarme Architektur-Slice ist die Zerlegung von `resource-read-shared.ts` in fokussierte Read-Helper bei stabilem API-Vertrag. diff --git a/docs/assistant-tool-test-split-migration.md b/docs/assistant-tool-test-split-migration.md index 9d1d559..8b515c6 100644 --- a/docs/assistant-tool-test-split-migration.md +++ b/docs/assistant-tool-test-split-migration.md @@ -156,9 +156,11 @@ Abgedeckt werden damit insbesondere: Der aktuelle Split-Runner wurde gegen die genannten Batches, die dedizierten Gap-Closure-Suiten und den API-Typecheck validiert. -## Bekannte Restlücken +## Migration abgeschlossen -Aktuell sind für den migrierten Legacy-Scope keine weiteren isolierten Split-Lücken dokumentiert. +Der Legacy-Scope der entfernten Monolith-Suiten ist vollständig auf fachlich geschnittene Split-Suiten gemappt. +Neue Assistant-Regressionen sollen direkt in die fachlich passende Split-Suite eingeordnet werden. +Der explizite Split-Runner bleibt für diesen Scope der kanonische Regressionspfad; breite Glob-Runs und eine Wiederbelebung der alten Monolith-Dateien sind bewusst nicht vorgesehen. Bewusst noch nicht Teil dieses Dokuments: diff --git a/docs/domain-slices-backlog.md b/docs/domain-slices-backlog.md index 99f39e4..791a46e 100644 --- a/docs/domain-slices-backlog.md +++ b/docs/domain-slices-backlog.md @@ -6,7 +6,7 @@ Dieses Backlog gruppiert die offenen Arbeiten nach fachlichen/domänischen Blöc Die Schätzungen sind Restaufwand auf Basis des aktuellen Worktrees und können sich nach Befund leicht ändern. Aktuell geschnittene Domänenslices: `7` -Grobe Gesamtdauer über alle noch offenen Slices: `28-45h` +Grobe Gesamtdauer über alle noch offenen Slices: `25-41h` ## Statuslegende @@ -20,12 +20,12 @@ Grobe Gesamtdauer über alle noch offenen Slices: `28-45h` | # | Slice | Status | Restaufwand | Kernziel | |---|---|---|---:|---| | 1 | Timeline Interaction, Overlays, Hover, SSE-State | in_progress | 6-10h | Stabile Timeline ohne Render-/Overlay-/Drag-Artefakte, auch bei View-Wechseln und Live-Updates | -| 2 | Holiday/Vacation Correctness and Explainability | ready | 4-6h | Feiertage und Urlaube konsistent in Timeline, Chargeability, Forecasts, Assistant und UI-Erklärungen | -| 3 | Assistant Parity, Policy Auto-Exposure, Advanced Queries | ready | 5-8h | Assistant kennt und nutzt denselben Berechtigungs- und Datenraum wie der Nutzer, inkl. komplexer Queries | -| 4 | Reports, Templates/Blueprints, Export Completeness | ready | 4-6h | Report Builder mit speicherbaren Vorlagen und vollständigen Exportgrößen wie SAH und Bezugsfaktoren | -| 5 | Notifications, Tasks, Broadcasts, Reminder Reliability | ready | 3-5h | Robuste Inbox-/Broadcast-/Reminder-Flows ohne leere Fanout- oder Persistenzkanten | -| 6 | Dashboard, Widgets, Explainability, First-Load Stability | ready | 4-6h | Dashboard zuverlässig ladbar, Widgets kompakt, nachvollziehbar und per Detail-Toggle kontrollierbar | -| 7 | DB Safety, Env Loading, Migration/Seed Discipline, Naming Cleanup | ready | 2-4h | Keine Raw-Prisma/Env-Fallen, kein versehentliches Seeding, keine operativen `planarchy`-Reste | +| 2 | Holiday/Vacation Correctness and Explainability | in_progress | 1-3h | Feiertage und Urlaube konsistent in Timeline, Chargeability, Forecasts, Assistant und UI-Erklärungen | +| 3 | Assistant Parity, Policy Auto-Exposure, Advanced Queries | in_progress | 5-8h | Assistant kennt und nutzt denselben Berechtigungs- und Datenraum wie der Nutzer, inkl. komplexer Queries | +| 4 | Reports, Templates/Blueprints, Export Completeness | in_progress | 4-6h | Report Builder mit speicherbaren Vorlagen und vollständigen Exportgrößen wie SAH und Bezugsfaktoren | +| 5 | Notifications, Tasks, Broadcasts, Reminder Reliability | in_progress | 3-5h | Robuste Inbox-/Broadcast-/Reminder-Flows ohne leere Fanout- oder Persistenzkanten | +| 6 | Dashboard, Widgets, Explainability, First-Load Stability | in_progress | 4-6h | Dashboard zuverlässig ladbar, Widgets kompakt, nachvollziehbar und per Detail-Toggle kontrollierbar | +| 7 | DB Safety, Env Loading, Migration/Seed Discipline, Naming Cleanup | in_progress | 2-4h | Keine Raw-Prisma/Env-Fallen, kein versehentliches Seeding, keine operativen `planarchy`-Reste | ## Umsetzungshinweise @@ -34,21 +34,138 @@ Grobe Gesamtdauer über alle noch offenen Slices: `28-45h` - Slice 4 profitiert von Slice 2, weil Feiertags-/Urlaubslogik als Export- und Reportgröße sichtbar werden muss. - Slice 7 bleibt als Guardrail-Block parallel im Hintergrund wichtig, sollte aber keine Produktarbeit blockieren. +## Arbeitsliste + +1. Slice 1: Timeline Interaction, Overlays, Hover, SSE-State +2. Slice 2: Holiday/Vacation Correctness and Explainability +3. Slice 3: Assistant Parity, Policy Auto-Exposure, Advanced Queries +4. Slice 4: Reports, Templates/Blueprints, Export Completeness +5. Slice 5: Notifications, Tasks, Broadcasts, Reminder Reliability +6. Slice 6: Dashboard, Widgets, Explainability, First-Load Stability +7. Slice 7: DB Safety, Env Loading, Migration/Seed Discipline, Naming Cleanup + ## Abhaklog - [x] Assistant-Testlandschaft entlang echter Domänengrenzen weiter zerlegt und vollständig grün validiert +- [x] Staffing Read Pipeline Split: gemeinsame holiday-/absence-aware Capacity-Zusammenfassung aus `staffing-suggestions-read.ts` und `staffing-best-project-resource.ts` in `staffing-capacity-summary.ts` extrahiert und gegen fokussierte Staffing-/Assistant-Regressionen validiert +- [x] Vacation Read Separation: Holiday-/Deduction-Preview-Shaping aus `vacation-read.ts` in `vacation-read-support.ts` extrahiert und gegen Vacation-Auth-/Router-Regressionen validiert - [ ] Slice 1: Timeline Interaction, Overlays, Hover, SSE-State Fortschritt: Live `cellWidthRef` statt stale Snapshot im Drag-Pfad verdrahtet. Projekt-Drag zentralisiert finalisiert, sodass MouseUp/Canvas-Up nicht mehr parallel mutieren. `document`-Listener für Projekt-Drag, Allocation-Drag und Multi-Select werden beim Unmount sauber entfernt. Timeline-Popovers schließen bei Scroll/Resize nicht mehr sofort, sondern bleiben stabil sichtbar. + Query-Refresh wird pro aktivem Timeline-Kontext statt nur einmal pro Mount erzwungen. + Session-Loading und SSE-Reconnect werden gehärtet, damit erste Route-Loads nicht in einem leeren Zwischenzustand hängen bleiben. + Vacation-SSE und lokale Planning-Invalidierung laden jetzt auch `vacation.list` nach, damit Urlaubsbalken ohne Reload konsistent bleiben. + Allocation-Handles und Move-Flächen stoppen jetzt `mousedown` sauber, damit Resource-/Project-Row-Canvases nicht parallel Range-Selektion starten. - [ ] Slice 2: Holiday/Vacation Correctness and Explainability + Fortschritt: + Vacation-Deduction-Snapshots werden im UI bereits in `MyVacationsClient` erklärt. + Dieselbe Ableitung wird jetzt auch im Vacation-Kalender per Hover-Tooltip genutzt. + Holiday-Editor-Vorschau zeigt jetzt Scope-/Source-Zusammenfassung plus aufgeloeste Feiertage mit Scope. + `entitlement.getBalanceDetail`, Assistant-Tool `get_vacation_balance` und `BalanceCard` nutzen jetzt denselben holiday-/deduction-aware Explainability-Pfad inkl. Vacation-Breakdown, Holiday-Basis und ausgeschlossenen Feiertagen. + Fokussierte Validierung für Entitlement-/Assistant-Balance und Web-Typecheck ist grün; Alt-Snapshots respektieren weiter persistierte `deductedDays`, während fehlende Snapshots wieder korrekt auf kanonische Feiertagsauflösung zurückfallen. + Entitlement-Jahresübersicht, Assistant-`get_entitlement_summary` und die Manager-Übersicht tragen jetzt auch `countryCode`, `countryName`, `federalState` und `metroCityName`, damit regionale Feiertagsunterschiede als Bezugsgröße sichtbar bleiben. + Assistant-`run_report` liefert für `resource_month` jetzt zusätzlich einen Explainability-Block mit Holiday-/Absence-/Location-Basis, fehlenden empfohlenen Bezugsgrößen und Interpretationshinweisen zur SAH-Ableitung. + Chargeability-Detailpfad und Assistant-`get_chargeability_report` tragen jetzt ebenfalls einen Explainability-Block mit Ortsbezug, Herleitungsfeldern, Formeln und aktiven Filtern, damit dieselbe SAH-/Holiday-/Absence-Logik auch im Forecast-Readmodel transparent ist. + Chargeability-Frontend zeigt dieselbe Berechnungsbasis jetzt kompakt an; der Excel-Export enthält zusätzlich ein Explainability-Sheet mit Formeln, Filtern und Bezugsgrößen. + Budget-Forecast-Readmodel, Dashboard-Detail und Assistant-`get_budget_forecast` tragen jetzt ebenfalls eine kompakte Burn-Herleitung mit Base-vs-Adjusted-Burn, Holiday-/Absence-Abzügen, Holiday-aware-vs-Fallback-Assignments und Anzahl der Kalenderkontexte; das Widget zeigt diese Herleitung nur im Detailmodus an. + Project-Health-Readmodel, Dashboard-Detail und Assistant-`get_project_health` tragen jetzt ebenfalls eine explizite Budget-Herleitung mit Base-vs-Adjusted-Spend, Holiday-/Absence-Abzügen, Holiday-aware-vs-Fallback-Assignments und Kalenderkontexten; das Widget zeigt diese Basis nur im Detailmodus an. + Nächster Fokus: verbleibende Forecast-/Widget-Parität derselben Ableitungen schließen. - [ ] Slice 3: Assistant Parity, Policy Auto-Exposure, Advanced Queries + Fortschritt: + Tool-Sichtbarkeit und Laufzeit-Guarding auf gemeinsame Registry-/Access-Metadaten gezogen. + Estimate-Tools tragen jetzt explizite Rollen-/Permission-/Cost-Metadaten statt impliziter Alt-Policy. + Fokussierte Assistant-Policy-Regressionstests laufen gegen dieselbe zentrale Quelle. + Tool-Selektion priorisiert Holiday-/Vacation-/Entitlement- sowie Dashboard-/Report-Anfragen jetzt gezielter, damit diese Tools unter dem OpenAI-128-Limit erhalten bleiben. + Top-Level-Chat-/Approval-/Prompt-Orchestrierung ist jetzt in `assistant-procedure-support.ts` und `assistant-system-prompt.ts` extrahiert; API-Typecheck plus fokussierte Assistant-Suite laufen wieder grün. + Produktive AI-Fallbacks, Admin-UI-Platzhalter und Settings-Default im Frontend zeigen jetzt konsistent `gpt-5.4` statt alter `gpt-4o-mini`-Defaults; die fokussierte Assistant-/Settings-/Insights-Suite dafür ist grün validiert. - [ ] Slice 4: Reports, Templates/Blueprints, Export Completeness + Fokus: + Report-Templates/Blueprints, SAH-/Bezugsgrößen-Vollständigkeit und Export-Parität zur UI weiterziehen. + Fortschritt: + Report Builder exponiert Template-Beschreibung und Shared-Status jetzt auch im Frontend statt nur im Backend-Vertrag. + Gespeicherter Template-Status vs. lokal geänderte Builder-Konfiguration wird jetzt explizit angezeigt, damit lokale Draft-Änderungen nicht mehr wie persistierte Vorlagen wirken. + Duplicate-Template-Saves werden jetzt fachlich als `CONFLICT` mit verständlicher Meldung statt als roher DB-Fehler zurückgegeben. + `resource_month`-Explainability ist jetzt im gemeinsamen Report-Pfad statt nur im Assistant-Sonderfall verdrahtet; der Builder zeigt dieselbe Holiday-/Absence-/SAH-Basis kompakt im Result-Header an. + `resource_month`-Exporte laufen jetzt als XLSX mit separatem Explainability-Sheet, damit SAH-/Kalender-/Feiertagslogik im Export nachvollziehbar bleibt ohne Logik in Excel neu aufzubauen. + Der `resource_month`-Blueprint-/Preset-Katalog kommt jetzt aus einem gemeinsamen Backend-Vertrag inklusive Runtime-Output-Schema statt aus lokalem Frontend-Hardcode; fokussierte Router-/Typecheck-Validierung dafür ist grün. + `resource`, `project` und `assignment` tragen jetzt zusätzliche Bezugsgrößen wie Blueprint, Client Unit, Enterprise ID, Value Score, Shoring-/Onshore-Felder sowie Resource-/Project-Kontext an Assignments in den Report-Spaltenkatalog. + Nächster Fokus: verbleibende Report-/Export-Vollständigkeit an echten Nutzerflows prüfen, insbesondere Frontend-/Export-Verhalten außerhalb von `resource_month`. - [ ] Slice 5: Notifications, Tasks, Broadcasts, Reminder Reliability + Fortschritt: + Notification-/Reminder-/Webhook-Testtrio ist grün validiert. + Zukünftige Scheduled Broadcasts mit Task-/Approval-Metadaten werden jetzt hart abgewiesen, damit `taskAction`/`dueDate` nicht still verloren gehen. + Broadcast-Fanout mappt jetzt auch verlorene `sourceId`-/Sender-Referenzen stabil auf fachliche `NOT_FOUND`-Fehler statt rohe FK-Fehler. + Nicht-terminale Task-Statuswechsel bereinigen jetzt `completedAt`/`completedBy` deterministisch, damit wiedergeöffnete Tasks fachlich und technisch konsistent bleiben. + Wiederkehrende Reminder ziehen `nextRemindAt` nach Downtime jetzt direkt auf den ersten Termin nach `now` vor, damit überfällige Serien-Erinnerungen nicht auf jedem Scheduler-Tick erneut feuern. + Task-Reassignment mappt fehlende `assigneeId`-Referenzen jetzt über den gemeinsamen Notification-Fehlerpfad stabil auf `NOT_FOUND`, statt rohe FK-Fehler aus dem Core-Router nach oben zu reichen. + Immediate Broadcasts werden jetzt ohne `$transaction` hart abgewiesen, damit es keinen nicht-atomaren Fan-out-Fallback mit Teilpersistenz mehr gibt. + Broadcast-Fanout gegen fehlende Empfänger ist jetzt ebenfalls fokussiert abgesichert: `userId`-FK-Verlust im Recipient-Create mappt stabil auf fachliches `NOT_FOUND`, ohne Broadcast-Finalisierung, SSE oder E-Mail-Nebenwirkungen. + Direkte Router-Writes für `notification.create` und `notification.createTask` nutzen jetzt denselben kontextsensitiven Notification-Fehlerpfad wie Assistant/Broadcast, sodass fehlende Recipient-/Sender-Referenzen als fachliche `NOT_FOUND`-Fehler statt rohe Prisma-FKs herauskommen. + `executeTaskAction` blockt jetzt auch `DISMISSED` als terminalen Zustand und läuft nur noch transaktional, damit Domain-Aktion und Task-Abschluss nicht mehr auseinanderlaufen; die Assistant-Test-Helpers spiegeln denselben Transaction-Contract jetzt ebenfalls. + Reminder-Updates und -Deletes sind jetzt auch gegen Missing-/Wrong-Owner-Fälle fokussiert regressionsgesichert, damit die fachliche `NOT_FOUND`-Oberfläche stabil bleibt. + `assignTask` fängt jetzt auch den Read-then-Update-Race auf gelöschte Tasks sauber als fachliches `NOT_FOUND` ab; Missing-Task- und Wrong-Category-Pfade sind auf Router-Ebene zusätzlich abgesichert. + Nächster Fokus: verbleibende Persistenz-/Broadcast-Kanten schließen. - [ ] Slice 6: Dashboard, Widgets, Explainability, First-Load Stability + Fortschritt: + Peak Times komprimiert die Summary-Leiste jetzt in drei kompakte Pills statt großer redundanter Karten. + Zusatztexte und Erklärinfos bleiben dort jetzt konsequent hinter dem Details-Toggle. + Budget Forecast reduziert Summary-Helfertexte und sekundäre Zeilendetails jetzt ebenfalls sauber bei `Details off`. + Dashboard-Layout-Hydration überschreibt beim First Load lokale Nutzeraktionen nicht mehr, wenn DB-Layout und erste Interaktion race-condition-artig aufeinandertreffen; fokussierte Hook-/Widget-Tests dafür sind grün. + Top-Value-Ressourcen tragen jetzt auch `countryName` durch Readmodel, API-Detailpfad und Widget-Lokationsanzeige, damit Kalender-/Feiertagsbezug nicht nur als Kürzel sichtbar ist. + Top-Value trägt jetzt zusätzlich `valueScoreBreakdown` und `valueScoreUpdatedAt` bis in API-/Assistant-Detailpfade und Widget-Hover durch, damit die Score-Herleitung nachvollziehbar bleibt ohne den Default-View mit Text zu überladen. + Peak Times trägt jetzt pro Periode `calendarContextCount` plus kompakte `calendarLocations` aus dem Application-Layer bis in Widget-Hover, Detailpanel und `get_dashboard_detail`, damit regionale Feiertags-/Standortbasis auch für Auslastungsspitzen sichtbar bleibt. - [ ] Slice 7: DB Safety, Env Loading, Migration/Seed Discipline, Naming Cleanup + Fortschritt: + Repo-Wrapper für Env-Laden sind gesetzt; aktuelle Assistant-, Import/Export- und Insights-Tests sowie der Web-Typecheck laufen grün. + Operative Altspuren `planarchy` kommen derzeit noch über einen Symlink-Pfad und einzelne Logs/Artefakte hinein und werden weiter bereinigt. + `pnpm db:prisma -- ...` prüft jetzt für destruktive oder schemawirksame Prisma-Kommandos hart, dass `DATABASE_URL` wirklich auf `capakraken` zeigt; falsche Ziele wie `planarchy` werden vor jedem Prisma-Zugriff blockiert. + Aktive Worker: + Worker A: Slice 5 Notification-/Broadcast-Persistenzkanten. + Worker B: Slice 2 Holiday-/Vacation-Explainability in Forecast-/Widget-Parität. + Lokal: Slice 1 Timeline-Stabilität, Slice 3 Assistant-Parity, Slice 7 DB-/Env-Guardrails und Naming-Cleanup. + +## Validierter Stand + +- Assistant-Regressionen: `8` fokussierte Testdateien, `36` Tests grün. +- Assistant-/Settings-/Insights-/AI-Defaults: `8` fokussierte Testdateien, `30` Tests grün. +- Web-Typecheck: `pnpm --filter @capakraken/web exec tsc -p tsconfig.typecheck.json --noEmit` grün. +- Timeline/SSE-Regressionen: `pnpm --filter @capakraken/api exec vitest run src/__tests__/timeline-router.test.ts src/__tests__/sse-subscription-policy.test.ts` grün (`15` Tests). +- Dashboard WidgetContainer: `pnpm --filter @capakraken/web exec vitest run src/components/dashboard/WidgetContainer.test.tsx` grün (`2` Tests). +- Insights Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/insights-router.test.ts src/__tests__/insights-procedure-support.test.ts` grün (`9` Tests). +- Country Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/country-router.test.ts src/__tests__/country-procedure-support.test.ts src/__tests__/country-support.test.ts` grün (`12` Tests). +- Holiday Calendar Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/holiday-calendar-router.test.ts src/__tests__/holiday-calendar-router-auth.test.ts src/__tests__/holiday-calendar-procedure-support.test.ts src/__tests__/holiday-calendar-support.test.ts src/__tests__/holiday-calendar-write-support.test.ts` grün (`26` Tests). +- Dispo Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/dispo-router.test.ts src/__tests__/dispo-procedure-support.test.ts src/__tests__/dispo-management-support.test.ts` grün (`10` Tests). +- Org Unit + Import/Export Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/org-unit-router.test.ts src/__tests__/org-unit-procedure-support.test.ts src/__tests__/org-unit-support.test.ts src/__tests__/import-export-router.test.ts src/__tests__/import-export-procedure-support.test.ts` grün (`19` Tests). +- Estimate Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/estimate-router.test.ts` grün (`45` Tests). +- Import/Export Procedure Support: `pnpm --filter @capakraken/api exec vitest run src/__tests__/import-export-procedure-support.test.ts` grün (`5` Tests). +- Assistant Tool Selection: `pnpm --filter @capakraken/api exec vitest run src/__tests__/assistant-tool-selection.test.ts` grün (`3` Tests). +- Assistant Split Regression Runner: `pnpm --filter @capakraken/api test:assistant-split` als explizite Fünf-Batch-Regression plus API-Typecheck etabliert; deckt jetzt auch `export_projects_csv`, Holiday-Resolution-Fehlerpfade und `get_timeline_holiday_overlays` ab. +- Assistant Report Read: `pnpm --filter @capakraken/api exec vitest run src/__tests__/assistant-tools-report-read.test.ts` grün (`3` Tests). +- Report Router: `pnpm --filter @capakraken/api exec vitest run src/__tests__/report-router.test.ts` grün (`8` Tests). +- Report Explainability/Export Parity: `pnpm --filter @capakraken/api exec vitest run src/__tests__/report-router.test.ts src/__tests__/assistant-tools-report-read.test.ts` grün (`11` Tests), `pnpm --filter @capakraken/web exec vitest run src/components/reports/reportBuilderExplainability.test.ts` grün (`2` Tests). +- Report Blueprint Catalog Contract: `pnpm --filter @capakraken/api exec vitest run src/__tests__/report-router.test.ts` grün (`9` Tests), `pnpm --filter @capakraken/api exec tsc -p tsconfig.json --noEmit --pretty false` grün, `pnpm --filter @capakraken/web exec vitest run src/components/reports/reportBuilderExplainability.test.ts` grün (`2` Tests). +- Report Column Coverage Expansion: `pnpm --filter @capakraken/api exec vitest run src/__tests__/report-router.test.ts` grün (`11` Tests), `pnpm --filter @capakraken/api exec tsc -p tsconfig.json --noEmit --pretty false` grün. +- Chargeability Report Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/chargeability-report-router.test.ts src/__tests__/assistant-tools-chargeability-report.test.ts` grün (`7` Tests). +- Notification Router + Reminder Scheduler: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-router.test.ts src/__tests__/reminder-scheduler.test.ts` grün (`41` Tests). +- Project Health Explainability: `pnpm --filter @capakraken/application exec vitest run src/__tests__/dashboard.test.ts -t "excludes regional public holidays from project health budget usage"` grün (`1` Test), `pnpm --filter @capakraken/api exec vitest run src/__tests__/dashboard-router.test.ts -t "getProjectHealthDetail"` grün (`1` Test), `pnpm --filter @capakraken/api exec vitest run src/__tests__/assistant-tools-dashboard-project-health.test.ts` grün (`1` Test). +- Dashboard Top Value Country Context: `pnpm --filter @capakraken/api exec vitest run src/__tests__/dashboard-procedure-support.test.ts src/__tests__/dashboard-router.test.ts` grün (`19` Tests). +- Dashboard Top Value Explainability: `pnpm --filter @capakraken/application exec vitest run src/__tests__/dashboard.test.ts -t "enforces visible-role filtering for top value resources"` grün (`1` Test), `pnpm --filter @capakraken/api exec vitest run src/__tests__/dashboard-procedure-support.test.ts src/__tests__/dashboard-router.test.ts src/__tests__/assistant-tools-dashboard-detail.test.ts` grün (`20` Tests), `pnpm --filter @capakraken/web exec vitest run src/components/dashboard/widgets/TopValueWidget.test.tsx` grün (`1` Test). +- Peak Times Calendar Explainability: `pnpm --filter @capakraken/application exec vitest run src/__tests__/dashboard.test.ts -t "peak times"` grün (`4` Tests aktiv, `17` geskippt), `pnpm --filter @capakraken/web exec vitest run src/components/dashboard/widgets/PeakTimesWidget.test.tsx` grün (`1` Test), `pnpm --filter @capakraken/api exec vitest run src/__tests__/dashboard-procedure-support.test.ts src/__tests__/assistant-tools-dashboard-detail.test.ts` grün (`4` Tests). +- Notification Task Reopen Reliability: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-router.test.ts` grün (`37` Tests). +- Notification Task Assignment Reference Guard: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-procedure-support.test.ts src/__tests__/notification-router.test.ts` grün (`46` Tests). +- Notification Immediate Broadcast Transaction Guard: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-router.test.ts` grün (`40` Tests). +- Notification Broadcast Recipient Reference Guard: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-procedure-support.test.ts src/__tests__/notification-router.test.ts` grün (`49` Tests), `pnpm --filter @capakraken/api exec tsc -p tsconfig.json --noEmit --pretty false` grün. +- Notification Direct Create/CreateTask Reference Guard: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-procedure-support.test.ts src/__tests__/notification-router.test.ts src/__tests__/assistant-tools-notification-create-errors.test.ts src/__tests__/assistant-tools-task-create-errors.test.ts` grün (`60` Tests), `pnpm --filter @capakraken/api exec tsc -p tsconfig.json --noEmit --pretty false` grün. +- Notification Task Action Transaction Guard: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-router.test.ts src/__tests__/assistant-tools-task-action-guards.test.ts src/__tests__/assistant-tools-task-action-execution.test.ts src/__tests__/assistant-tools-task-action-assignment-errors.test.ts src/__tests__/assistant-tools-task-action-vacation-errors.test.ts` grün (`56` Tests), `pnpm --filter @capakraken/api exec tsc -p tsconfig.json --noEmit --pretty false` grün. +- Notification Reminder Ownership + AssignTask Race Guard: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-router.test.ts` grün (`51` Tests), `pnpm --filter @capakraken/api exec tsc -p tsconfig.json --noEmit --pretty false` grün. +- Notification Support + Assistant Router: `pnpm --filter @capakraken/api exec vitest run src/__tests__/notification-procedure-support.test.ts src/__tests__/notification-router.test.ts src/__tests__/notification-router-auth.test.ts src/__tests__/assistant-router.test.ts src/__tests__/assistant-router-auth.test.ts src/__tests__/assistant-chat-loop.test.ts src/__tests__/assistant-chat-response.test.ts src/__tests__/assistant-tool-selection.test.ts src/__tests__/assistant-approvals.test.ts src/__tests__/assistant-procedure-support.test.ts` grün (`74` Tests). +- Assistant Broadcast Validation: `pnpm --filter @capakraken/api exec vitest run src/__tests__/assistant-tools-broadcast-send-validation-errors.test.ts` grün (`3` Tests). +- Staffing Read Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/staffing-router.test.ts src/__tests__/assistant-tools-holiday-staffing-suggestions.test.ts src/__tests__/assistant-tools-advanced-resource-ranking.test.ts` grün (`32` Tests). +- Vacation Read Slice: `pnpm --filter @capakraken/api exec vitest run src/__tests__/vacation-router.test.ts src/__tests__/vacation-router-auth.test.ts` grün (`58` Tests). +- API-Typecheck: `pnpm --filter @capakraken/api exec tsc -p tsconfig.json --noEmit --pretty false` grün. ## API Router Slice Inventory @@ -71,29 +188,29 @@ oder benachbarte Support-Module, Verhalten und öffentliche Contracts bleiben st | Router | Status | Priorität | Sinnvolle Slices | |---|---|---:|---| | `allocation.ts` | done | - | Aggregator bereits dünn; nur Support-Module weiter pflegen | -| `assistant.ts` | later | 3 | Chat loop, confirmation flow, insight generation, response shaping | +| `assistant.ts` | done | - | top-level chat, approval payloads und system prompt in Support-Module extrahiert | | `audit-log.ts` | monitor | 4 | Optional: list/detail reads und filter-building trennen, falls Auth/Wiring wächst | -| `blueprint.ts` | ready_now | 2 | summary/list reads, identifier reads, CRUD, role-preset mutation, global-field procedures | +| `blueprint.ts` | done | - | summary/list reads, identifier reads, CRUD, role-preset mutation und global-field procedures in `blueprint-procedure-support.ts` extrahiert | | `calculation-rules.ts` | monitor | 4 | Aktuell klein; nur bei zusätzlicher Write-/Audit-Logik extrahieren | | `chargeability-report.ts` | excluded_for_now | - | Bewusst zurückgestellt; erst nach Freigabe des geblockten Bereichs | -| `client.ts` | ready_now | 2 | list/tree reads, identifier reads, CRUD/deactivate, sort-order batch mutation | +| `client.ts` | done | - | list/tree reads, identifier reads, CRUD/deactivate und sort-order batch mutation in `client-procedure-support.ts` extrahiert | | `comment.ts` | done | - | Bereits in `comment-procedure-support.ts` ausgelagert | | `computation-graph.ts` | monitor | 4 | Bereits Kompositionsrouter; nur bei zusätzlicher Orchestrierung weiter schneiden | -| `country.ts` | ready_now | 2 | list/identifier reads, country CRUD, metro-city CRUD | +| `country.ts` | done | - | list/identifier reads, country CRUD, metro-city CRUD | | `dashboard.ts` | excluded_for_now | - | Bewusst zurückgestellt; hoher Querschnitt und paralleler Scope | -| `dispo.ts` | ready_now | 2 | workbook input schemas, staging/validation, staged-read procedures, resolve/commit/cancel mutations | -| `effort-rule.ts` | ready_now | 1 | list/detail reads, CRUD/default toggle, preview, apply-rules mutation | +| `dispo.ts` | done | - | workbook input schemas, staging/validation, staged-read procedures, resolve/commit/cancel mutations | +| `effort-rule.ts` | done | - | list/detail reads, CRUD/default toggle, preview und apply-rules mutation in `effort-rule-procedure-support.ts` extrahiert | | `entitlement.ts` | excluded_for_now | - | Bewusst zurückgestellt; größerer Fachblock | -| `estimate.ts` | later | 3 | commercial reads, demand line writes, phasing, version workflow, aggregate read models | -| `experience-multiplier.ts` | ready_now | 1 | list/detail reads, CRUD/default toggle, preview, apply-rules mutation | -| `holiday-calendar.ts` | ready_now | 2 | calendar CRUD, entry CRUD; catalog/resolution reads sind schon separat | -| `import-export.ts` | ready_now | 3 | export reads, import preview/validation, file-bound orchestration | -| `insights.ts` | ready_now | 3 | anomaly/summary reads, cached narrative read, AI narrative mutation | -| `management-level.ts` | ready_now | 1 | group reads/writes, level writes/delete, audit helper extraction | -| `notification.ts` | excluded_for_now | - | Bewusst zurückgestellt; zu groß für den aktuellen Batch | -| `org-unit.ts` | ready_now | 3 | list/tree reads, identifier reads, CRUD/deactivate | +| `estimate.ts` | done | - | commercial reads, demand line writes, phasing, version workflow, aggregate read models | +| `experience-multiplier.ts` | done | - | list/detail reads, CRUD/default toggle, preview und apply-rules mutation in `experience-multiplier-procedure-support.ts` extrahiert | +| `holiday-calendar.ts` | done | - | calendar CRUD, entry CRUD; catalog/resolution reads sind schon separat | +| `import-export.ts` | done | - | export reads, import preview/validation, file-bound orchestration | +| `insights.ts` | done | - | anomaly/summary reads, cached narrative read, AI narrative mutation | +| `management-level.ts` | done | - | group reads/writes, level writes/delete und Audit-Orchestrierung in `management-level-procedure-support.ts` extrahiert | +| `notification.ts` | later | 3 | Top-Level ist tragbar; nächster sinnvoller Schritt ist die weitere Zerlegung von `notification-procedure-support.ts` | +| `org-unit.ts` | done | - | list/tree reads, identifier reads, CRUD/deactivate | | `project.ts` | monitor | 4 | Bereits stark komponiert; optional nur `list` und `getById` separat ziehen | -| `rate-card.ts` | ready_now | 1 | list/detail/match reads, card CRUD, line CRUD, replace-lines/batch operations | +| `rate-card.ts` | done | - | list/detail/match reads, card CRUD, line CRUD und replace-lines in `rate-card-procedure-support.ts` extrahiert | | `report.ts` | done | - | Bereits in `report-template-procedure-support.ts` ausgelagert | | `resource.ts` | monitor | 4 | Top-Level dünn; tieferliegende Shared-Read-Module sind separat zu betrachten | | `role.ts` | done | - | Bereits in `role-procedure-support.ts` ausgelagert | @@ -109,18 +226,12 @@ oder benachbarte Support-Module, Verhalten und öffentliche Contracts bleiben st ### Empfohlene Reihenfolge -1. `rate-card.ts` -2. `effort-rule.ts` -3. `experience-multiplier.ts` -4. `management-level.ts` -5. `blueprint.ts` -6. `client.ts` -7. `country.ts` -8. `holiday-calendar.ts` -9. `dispo.ts` -10. `insights.ts` -11. `import-export.ts` -12. `org-unit.ts` +1. Phase-2-Hotspots mit `Vacation Read Separation` +2. Phase-2-Hotspots mit `Staffing Read Pipeline Split` +3. `notification.ts` via weiterer Zerlegung von `notification-procedure-support.ts` +4. `entitlement.ts` nach Abschluss der Holiday-/Vacation-Parität +5. `dashboard.ts` nur bei erneutem Wachstum im API-Router +6. `user.ts` nur, falls dort wieder Top-Level-Orchestrierung anwächst ### Phase 2: Tieferliegende Hotspots Hinter Bereits Dünnen Routern @@ -131,6 +242,7 @@ im selben Architektur-Track. Sie sind sinnvoll, sobald der aktuelle Router-Arbei |---|---:|---|---| | Allocation Mutation Decomposition | 1 | `allocation-assignment-procedures.ts` | Assignment-Transaktionen, Audit, Events, Webhooks und Budget-Folgelogik weiter trennen | | Vacation Read Separation | 1 | `vacation-read.ts`, `vacation-management-procedures.ts` | Ownership, Anonymisierung, Holiday-Preview und Query-Varianten sauberer schneiden | +| Resource Shared Read Decomposition | 1 | `resource-read-shared.ts`, `resource-summary-read-procedure-support.ts`, `resource-identifier-read.ts` | Summary-/Detail-Selects, Identifier-Selects und Mapper aus `resource-read-shared.ts` in fokussierte Helper extrahieren; oeffentliche Read-Vertraege stabil halten | | Estimate Write Router Slimming | 1 | `estimate.ts` plus benachbarte Estimate-Module | Write-Orchestrierung, version workflow und error translation aus dem Router ziehen | | Staffing Read Pipeline Split | 1 | `staffing-suggestions-read.ts`, `staffing-capacity-read.ts`, `staffing-best-project-resource.ts` | gemeinsame Load-/Ranking-/Response-Pipeline extrahieren | | Report Engine by Execution Mode | 2 | `report-query-engine.ts`, `report-query-config.ts` | generische Reports, `resource_month`-Sonderfall und Exportpfade klar trennen | @@ -144,7 +256,6 @@ im selben Architektur-Track. Sie sind sinnvoll, sobald der aktuelle Router-Arbei - `packages/api/src/router/dashboard.ts` - `packages/api/src/router/entitlement.ts` - `packages/api/src/router/notification.ts` -- `packages/api/src/router/resource-read-shared.ts` - `packages/api/src/router/resource-summary-read.ts` - `packages/api/src/router/user.ts` - `packages/api/src/__tests__/timeline-router.test.ts` @@ -159,3 +270,17 @@ im selben Architektur-Track. Sie sind sinnvoll, sobald der aktuelle Router-Arbei - `report`: report template procedures extrahiert - `role`: role write procedures extrahiert - `settings`: settings procedures extrahiert +- `rate-card`: rate-card procedures extrahiert +- `effort-rule`: effort-rule procedures extrahiert +- `experience-multiplier`: experience-multiplier procedures extrahiert +- `management-level`: management-level procedures extrahiert +- `blueprint`: blueprint procedures extrahiert +- `client`: client procedures extrahiert +- `country`: read-/write-procedures in `country-procedure-support.ts` validiert +- `holiday-calendar`: write-procedures in `holiday-calendar-procedure-support.ts` validiert +- `dispo`: staged import/read/write procedures in `dispo-procedure-support.ts` validiert +- `import-export`: file-bound orchestration in `import-export-procedure-support.ts` validiert +- `insights`: procedure support und nullable-settings-Haertung validiert +- `org-unit`: read-/write-procedures in `org-unit-procedure-support.ts` validiert +- `estimate`: write/orchestration, rate lookup und Fehlertranslation in `estimate-procedure-support.ts` validiert +- `assistant`: chat-/approval-/prompt orchestration in `assistant-procedure-support.ts` und `assistant-system-prompt.ts` validiert diff --git a/packages/api/src/__tests__/resource-owned-read-access.test.ts b/packages/api/src/__tests__/resource-owned-read-access.test.ts new file mode 100644 index 0000000..05bb114 --- /dev/null +++ b/packages/api/src/__tests__/resource-owned-read-access.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it, vi } from "vitest"; +import { + assertCanReadOwnedResource, + canManageOwnedResourceReads, + findOwnedReadResourceId, + resolveOwnedResourceReadFilter, +} from "../router/resource-owned-read-access.js"; + +function createContext(options: { + role?: string | null; + userId?: string | null; + resourceFindFirst?: ReturnType; +} = {}) { + return { + dbUser: options.userId === null + ? null + : { + id: options.userId ?? "user_1", + systemRole: options.role ?? "USER", + }, + db: { + resource: options.resourceFindFirst + ? { findFirst: options.resourceFindFirst } + : { + findFirst: vi.fn().mockResolvedValue({ id: "res_own" }), + }, + }, + }; +} + +describe("resource-owned-read-access", () => { + it("treats admins and managers as broad readers", () => { + expect(canManageOwnedResourceReads(createContext({ role: "ADMIN" }))).toBe(true); + expect(canManageOwnedResourceReads(createContext({ role: "MANAGER" }))).toBe(true); + expect(canManageOwnedResourceReads(createContext({ role: "USER" }))).toBe(false); + }); + + it("finds the signed-in user's owned resource id", async () => { + const resourceFindFirst = vi.fn().mockResolvedValue({ id: "res_123" }); + + await expect(findOwnedReadResourceId(createContext({ resourceFindFirst }))).resolves.toBe("res_123"); + expect(resourceFindFirst).toHaveBeenCalledWith({ + where: { userId: "user_1" }, + select: { id: true }, + }); + }); + + it("scopes regular readers to their own resource filter", async () => { + await expect( + resolveOwnedResourceReadFilter( + createContext({ role: "USER" }), + undefined, + "forbidden", + ), + ).resolves.toBe("res_own"); + }); + + it("preserves the requested filter for managers", async () => { + await expect( + resolveOwnedResourceReadFilter( + createContext({ role: "MANAGER" }), + "res_other", + "forbidden", + ), + ).resolves.toBe("res_other"); + }); + + it("rejects access to another resource for regular readers", async () => { + await expect( + assertCanReadOwnedResource( + createContext({ role: "USER" }), + "res_other", + "forbidden", + ), + ).rejects.toMatchObject({ + code: "FORBIDDEN", + message: "forbidden", + }); + }); +}); diff --git a/packages/api/src/router/holiday-calendar-resolution-read.ts b/packages/api/src/router/holiday-calendar-resolution-read.ts index bce38e6..ea4c646 100644 --- a/packages/api/src/router/holiday-calendar-resolution-read.ts +++ b/packages/api/src/router/holiday-calendar-resolution-read.ts @@ -1,48 +1,20 @@ import { PreviewResolvedHolidaysSchema } from "@capakraken/shared"; -import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; import { asHolidayResolverDb, getResolvedCalendarHolidays } from "../lib/holiday-availability.js"; import { protectedProcedure } from "../trpc.js"; import { type HolidayReadContext } from "./holiday-calendar-shared.js"; - -function canManageHolidayResourceReads(ctx: HolidayReadContext): boolean { - const role = ctx.dbUser?.systemRole; - return role === "ADMIN" || role === "MANAGER"; -} - -async function findOwnedHolidayResourceId(ctx: HolidayReadContext): Promise { - if (!ctx.dbUser?.id) { - return null; - } - - if (!ctx.db.resource || typeof ctx.db.resource.findFirst !== "function") { - return null; - } - - const resource = await ctx.db.resource.findFirst({ - where: { userId: ctx.dbUser.id }, - select: { id: true }, - }); - - return resource?.id ?? null; -} +import { assertCanReadOwnedResource } from "./resource-owned-read-access.js"; async function assertCanReadHolidayResource( ctx: HolidayReadContext, resourceId: string, ): Promise { - if (canManageHolidayResourceReads(ctx)) { - return; - } - - const ownedResourceId = await findOwnedHolidayResourceId(ctx); - if (!ownedResourceId || ownedResourceId !== resourceId) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "You can only view holiday data for your own resource", - }); - } + await assertCanReadOwnedResource( + ctx, + resourceId, + "You can only view holiday data for your own resource", + ); } function formatResolvedHolidayDetail(holiday: { diff --git a/packages/api/src/router/resource-owned-read-access.ts b/packages/api/src/router/resource-owned-read-access.ts new file mode 100644 index 0000000..066c159 --- /dev/null +++ b/packages/api/src/router/resource-owned-read-access.ts @@ -0,0 +1,66 @@ +import { TRPCError } from "@trpc/server"; +import type { TRPCContext } from "../trpc.js"; + +export type OwnedResourceReadContext = Pick; + +export function canManageOwnedResourceReads(ctx: { dbUser: { systemRole: string } | null }): boolean { + const role = ctx.dbUser?.systemRole; + return role === "ADMIN" || role === "MANAGER"; +} + +export async function findOwnedReadResourceId( + ctx: OwnedResourceReadContext, +): Promise { + if (!ctx.dbUser?.id) { + return null; + } + + if (!ctx.db.resource || typeof ctx.db.resource.findFirst !== "function") { + return null; + } + + const resource = await ctx.db.resource.findFirst({ + where: { userId: ctx.dbUser.id }, + select: { id: true }, + }); + + return resource?.id ?? null; +} + +export async function assertCanReadOwnedResource( + ctx: OwnedResourceReadContext, + resourceId: string, + forbiddenMessage: string, +): Promise { + if (canManageOwnedResourceReads(ctx)) { + return; + } + + const ownedResourceId = await findOwnedReadResourceId(ctx); + if (!ownedResourceId || ownedResourceId !== resourceId) { + throw new TRPCError({ + code: "FORBIDDEN", + message: forbiddenMessage, + }); + } +} + +export async function resolveOwnedResourceReadFilter( + ctx: OwnedResourceReadContext, + requestedResourceId: string | undefined, + forbiddenMessage: string, +): Promise { + if (canManageOwnedResourceReads(ctx)) { + return requestedResourceId; + } + + const ownedResourceId = await findOwnedReadResourceId(ctx); + if (requestedResourceId && requestedResourceId !== ownedResourceId) { + throw new TRPCError({ + code: "FORBIDDEN", + message: forbiddenMessage, + }); + } + + return ownedResourceId; +} diff --git a/packages/api/src/router/vacation-read.ts b/packages/api/src/router/vacation-read.ts index 6303ce6..2812e1f 100644 --- a/packages/api/src/router/vacation-read.ts +++ b/packages/api/src/router/vacation-read.ts @@ -11,48 +11,27 @@ import { findVacationResourceChapter, listChapterVacationOverlaps, } from "./vacation-read-support.js"; +import { + assertCanReadOwnedResource, + canManageOwnedResourceReads, + resolveOwnedResourceReadFilter, +} from "./resource-owned-read-access.js"; type VacationReadContext = Pick; export function canManageVacationReads(ctx: { dbUser: { systemRole: string } | null }): boolean { - const role = ctx.dbUser?.systemRole; - return role === "ADMIN" || role === "MANAGER"; -} - -export async function findOwnedResourceId( - ctx: VacationReadContext, -): Promise { - if (!ctx.dbUser?.id) { - return null; - } - - if (!ctx.db.resource || typeof ctx.db.resource.findFirst !== "function") { - return null; - } - - const resource = await ctx.db.resource.findFirst({ - where: { userId: ctx.dbUser.id }, - select: { id: true }, - }); - - return resource?.id ?? null; + return canManageOwnedResourceReads(ctx); } export async function assertCanReadVacationResource( ctx: VacationReadContext, resourceId: string, ): Promise { - if (canManageVacationReads(ctx)) { - return; - } - - const ownedResourceId = await findOwnedResourceId(ctx); - if (!ownedResourceId || ownedResourceId !== resourceId) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "You can only view vacation data for your own resource", - }); - } + await assertCanReadOwnedResource( + ctx, + resourceId, + "You can only view vacation data for your own resource", + ); } export function isSameUtcDay(left: Date, right: Date): boolean { @@ -159,20 +138,14 @@ export const vacationReadProcedures = { }), ) .query(async ({ ctx, input }) => { - let resourceIdFilter = input.resourceId; + const resourceIdFilter = await resolveOwnedResourceReadFilter( + ctx, + input.resourceId, + "You can only view vacation data for your own resource", + ); - if (!canManageVacationReads(ctx)) { - const ownedResourceId = await findOwnedResourceId(ctx); - if (input.resourceId && input.resourceId !== ownedResourceId) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "You can only view vacation data for your own resource", - }); - } - if (!ownedResourceId) { - return []; - } - resourceIdFilter = ownedResourceId; + if (!canManageVacationReads(ctx) && !resourceIdFilter) { + return []; } const vacations = await ctx.db.vacation.findMany({