From dc5c5465faabd75e271fd4cad3809b2d01db028a Mon Sep 17 00:00:00 2001 From: Thorsten Hindermann Date: Sun, 19 Apr 2026 01:01:09 +0200 Subject: [PATCH 1/7] =?UTF-8?q?docs(001-pgsql-paritaet):=20complete=20plan?= =?UTF-8?q?=20quality=20checklist=20=E2=80=94=20all=2030=20items=20resolve?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - data-model.md: fix MachineState bool types, view descriptions (TRUE/FALSE), add PgSQL 14+ requirement, HardwareInventoryView semantic-change explanation, FK ON DELETE RESTRICT documentation, C#-Methoden-Abdeckung section - research.md: add R-07 negative daysToKeep edge case, R-08 unit/integration dividing line, R-10 Alternatives considered section - plan.md: document unquantified write-perf as accepted ambiguity, add CI filter commands, test-DB teardown, Dispose null-check to Null-Pattern - contracts/: NpgsqlException on all write methods, await using for CSV import, all 3 ServiceContainer changes with code examples, deferred stats-views section - quickstart.md: standardize PGSQL_TEST_CONNECTION_STRING env var, add SQL negative verification to Schritt 7 (WriteEnabled=false) - PgSqlDbService.cs: Disabled/Deprovisioned BOOLEAN + view WHERE clauses (from prev session) - checklists/plan.md: all 30 CHK items marked [x] Co-Authored-By: Claude Sonnet 4.6 --- .specify/memory/constitution.md | 97 +++- .specify/templates/tasks-template.md | 3 + CLAUDE.md | 3 + .../Services/Database/PgSqlDbService.cs | 14 +- Lastenheft_IDbService_Interface.md | 112 +++++ Lastenheft_MongoDB_Paritaet.md | 118 +++++ Lastenheft_PostgreSQL_Implementation.md | 34 +- Lastenheft_SQLite_ViewQuery_Bugfix.md | 83 ++++ Lastenheft_Statistik_View_Lesemethoden.md | 98 ++++ specs/001-pgsql-paritaet/checklists/plan.md | 267 ++++++++++ .../checklists/requirements.md | 36 ++ .../contracts/PgSqlDbService-methods.md | 468 ++++++++++++++++++ specs/001-pgsql-paritaet/data-model.md | 205 ++++++++ specs/001-pgsql-paritaet/plan.md | 223 +++++++++ specs/001-pgsql-paritaet/quickstart.md | 277 +++++++++++ specs/001-pgsql-paritaet/research.md | 244 +++++++++ specs/001-pgsql-paritaet/spec.md | 283 +++++++++++ 17 files changed, 2527 insertions(+), 38 deletions(-) create mode 100644 Lastenheft_IDbService_Interface.md create mode 100644 Lastenheft_MongoDB_Paritaet.md create mode 100644 Lastenheft_SQLite_ViewQuery_Bugfix.md create mode 100644 Lastenheft_Statistik_View_Lesemethoden.md create mode 100644 specs/001-pgsql-paritaet/checklists/plan.md create mode 100644 specs/001-pgsql-paritaet/checklists/requirements.md create mode 100644 specs/001-pgsql-paritaet/contracts/PgSqlDbService-methods.md create mode 100644 specs/001-pgsql-paritaet/data-model.md create mode 100644 specs/001-pgsql-paritaet/plan.md create mode 100644 specs/001-pgsql-paritaet/quickstart.md create mode 100644 specs/001-pgsql-paritaet/research.md create mode 100644 specs/001-pgsql-paritaet/spec.md diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index 6940f04..8917dce 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -1,29 +1,33 @@ # InventarWorkerService Constitution @@ -88,9 +92,13 @@ The `main` branch is protected and MUST NOT receive direct feature commits. Ever feature, fix, or constitutional amendment MUST be implemented on a newly created branch and merged through a pull request targeting `main`. Branches MAY use either the existing topic naming or the numbered Spec-Kit form `NNN-short-description`. Pull -requests MUST state purpose, touched projects, test evidence, and config/API impact; -UI-impacting changes in `InventarViewerApp` MUST include a screenshot or terminal -capture. +requests MUST state: +- Purpose and which projects are touched. +- Test evidence (coverage report or CI link). +- Config/API impact if applicable. +- UI-impacting changes in `InventarViewerApp` MUST include a screenshot or terminal + capture. +- Sample console output when user-visible output changes. Rationale: branch protection and documented review gates are mandatory for controlled integration. @@ -143,6 +151,25 @@ cost. compliance is reviewed before merge. 6. Perform a final documentation and coverage compliance review before merge. +### Commit Message Format + +Every commit MUST use Conventional Commits format: +`: ` + +Allowed types: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:`, `ci:`. +The subject line MUST be lowercase and imperative (e.g., `feat: add PgSqlDbService write methods`). + +Every commit that involves AI-assisted work MUST include a `Co-authored-by` trailer +identifying the active AI agent, for example: +``` +Co-Authored-By: Claude Opus 4.6 +``` +The exact identity token depends on the agent used in the session. The trailer MUST be +the last line of the commit message body. + +Rationale: uniform commit history enables automated changelog generation and makes +AI-assisted sessions auditable. + ### Statistical Documentation `docs/project-statistics.md` is the mandatory, living statistical ledger for the @@ -153,23 +180,25 @@ repository. It MUST be updated whenever one of the following happens: plans, tasks, governance, or operational docs). 3. A contributor explicitly requests a statistics refresh. - -Within the `## Fortschreibungsprotokoll` section, table rows MUST remain in strict chronological order: oldest entry first, newest and most recently added entry last, while rows with the same date keep their insertion order. +Within the `## Fortschreibungsprotokoll` section, table rows MUST remain in strict +chronological order: oldest entry first, newest and most recently added entry last, +while rows with the same date keep their insertion order. Every update MUST record, at minimum: - branch or phase identifier and current status, - observable git-based work window (first and last date, commit days where possible), -- current or change-based counts for production code, test code, and - documentation, +- current or change-based counts for production code, test code, and documentation, - the main work packages or delivered artefacts, - whether the numbers come from committed history, the working tree, or both, - a conservative manual-effort baseline using **80 manually created lines per workday** for an experienced developer across production code, test code, and documentation, - when time spans are derived, the assumptions for monthly conversion - (21-22 workdays, typically 21.5) and, if used, TVoeD-style annual leave - assumptions such as 30 vacation days per year. + (21.5 workdays/month) and TVöD-style annual leave (30 vacation days per year + through end of 2026, 31 days from 2027 onwards under a 5-day-week calendar), +- when hour values are shown, convert day-based estimates using the TVöD working-day + baseline of **7.8 hours (7h 48m)** per day. Manual-effort estimates for a small team MAY be derived from that baseline, but the formula and assumptions MUST be stated explicitly. @@ -187,7 +216,27 @@ versioning for governance: Compliance review is mandatory in planning and code review; unresolved violations MUST be documented in the implementation plan's complexity tracking section. +### Lastenheft Archivierung (Feature Completion Archive) + +When a feature's implementation is fully merged, the corresponding `Lastenheft_*.md` +MUST be renamed to stamp the delivering branch name onto the filename: + +```bash +# macOS/Linux +bash scripts/rename-lastenheft.sh + +# Windows +pwsh scripts/rename-lastenheft.ps1 -File -BranchName +``` + +Example: `Lastenheft_PostgreSQL_Implementation.md` + branch `008-pgsql-parity` +→ `Lastenheft_PostgreSQL_Implementation.008-pgsql-parity.md`. + +This rename MUST be included in the final tasks.md as the last task of the Polish +phase. Omitting it leaves the Lastenheft in an ambiguous delivered/undelivered state +and breaks traceability. + Use `docs/project-statistics.md` for the living project-statistics ledger and manual-effort baseline tracking. -**Version**: 2.4.0 | **Ratified**: 2026-03-08 | **Last Amended**: 2026-03-27 +**Version**: 2.5.0 | **Ratified**: 2026-03-08 | **Last Amended**: 2026-04-18 diff --git a/.specify/templates/tasks-template.md b/.specify/templates/tasks-template.md index 2020780..38cad53 100644 --- a/.specify/templates/tasks-template.md +++ b/.specify/templates/tasks-template.md @@ -165,6 +165,9 @@ Examples of foundational tasks (adjust based on your project): - [ ] TXXX Run quickstart.md validation - [ ] TXXX Run coverage report and attach CI evidence (>=70%; target >=80%) - [ ] TXXX Run `dotnet list package --outdated` and document package update decisions +- [ ] TXXX Rename the corresponding `Lastenheft_*.md` to `Lastenheft_*..md` + via `bash scripts/rename-lastenheft.sh ` (macOS/Linux) + or `pwsh scripts/rename-lastenheft.ps1 -File -BranchName ` (Windows) --- diff --git a/CLAUDE.md b/CLAUDE.md index cfcaf85..65b54c0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -166,3 +166,6 @@ InventarViewerApp (TUI) → queries InventarWorkerService API → persists in - Diese Datei ergaenzt die projektspezifische Dokumentation mit agentischen Arbeitsregeln. - This file complements the project-specific documentation with agent-oriented working rules. + +## Recent Changes +- 001-pgsql-paritaet: Added [if applicable, e.g., PostgreSQL, CoreData, files or N/A] diff --git a/InventarWorkerCommon/Services/Database/PgSqlDbService.cs b/InventarWorkerCommon/Services/Database/PgSqlDbService.cs index 9609358..2dbe47c 100644 --- a/InventarWorkerCommon/Services/Database/PgSqlDbService.cs +++ b/InventarWorkerCommon/Services/Database/PgSqlDbService.cs @@ -55,8 +55,8 @@ CreatedAt timestamptz DEFAULT now(), IPv4 TEXT, IPv6 TEXT, FQDN TEXT, - Disabled INTEGER NOT NULL DEFAULT 0, - Deprovisioned INTEGER NOT NULL DEFAULT 0, + Disabled BOOLEAN NOT NULL DEFAULT FALSE, + Deprovisioned BOOLEAN NOT NULL DEFAULT FALSE, LastHarvested timestamptz ); @@ -247,8 +247,8 @@ GROUP BY Architecture CREATE OR REPLACE VIEW AllActiveMachinesView AS SELECT Id, Name, IPv4, IPv6, FQDN, Disabled, Deprovisioned, LastSeen, LastHarvested FROM Machines - WHERE DISABLED = 0 AND DEPROVISIONED = 0; - + WHERE Disabled = FALSE AND Deprovisioned = FALSE; + -- View to retrieve all active machines with network information (not disabled or deprovisioned) -- This view selects all machines that are currently active, meaning they are not disabled or deprovisioned. -- It includes the Id, Name, IPv4, IPv6, FQDN, Disabled, Deprovisioned, LastSeen, and LastHarvested columns. @@ -262,7 +262,7 @@ FROM Machines CREATE OR REPLACE VIEW AllActiveMachinesWithNetworkInfoView AS SELECT Id, Name, IPv4, IPv6, FQDN, Disabled, Deprovisioned, LastSeen, LastHarvested FROM Machines - WHERE DISABLED = 0 AND DEPROVISIONED = 0 AND ( + WHERE Disabled = FALSE AND Deprovisioned = FALSE AND ( (IPv4 IS NOT NULL AND IPv4 != '') OR (IPv6 IS NOT NULL AND IPv6 != '') OR (FQDN IS NOT NULL AND FQDN != '') @@ -281,7 +281,7 @@ FROM Machines CREATE OR REPLACE VIEW AllDisabledMachinesView AS SELECT Id, Name, IPv4, IPv6, FQDN, Disabled, Deprovisioned, LastSeen, LastHarvested FROM Machines - WHERE DISABLED = 1 AND DEPROVISIONED = 0; + WHERE Disabled = TRUE AND Deprovisioned = FALSE; -- View to retrieve all deprovisioned machines (disabled) -- This view selects all machines that are currently deprovisioned, meaning they are disabled and marked as deprovisioned. @@ -297,7 +297,7 @@ FROM Machines CREATE OR REPLACE VIEW AllDeprovisionedMachinesView AS SELECT Id, Name, IPv4, IPv6, FQDN, Disabled, Deprovisioned, LastSeen, LastHarvested FROM Machines - WHERE DISABLED = 1 AND DEPROVISIONED = 1; + WHERE Disabled = TRUE AND Deprovisioned = TRUE; """; connection.Execute(createTablesAndViewsQuery); diff --git a/Lastenheft_IDbService_Interface.md b/Lastenheft_IDbService_Interface.md new file mode 100644 index 0000000..806e533 --- /dev/null +++ b/Lastenheft_IDbService_Interface.md @@ -0,0 +1,112 @@ +# Lastenheft: Einfuehrung des IDbService-Interfaces fuer Provider-Switching + +**Dokument-Status:** Entwurf +**Erstellt:** 2026-04-18 +**Betrifft:** `InventarWorkerCommon/Services/Database/SqliteDbService.cs`, `InventarWorkerCommon/Services/Database/PgSqlDbService.cs`, `InventarWorkerCommon/Services/Common/Initialize.cs`, `HarvesterWorkerService/Worker.cs` +**Prioritaet:** Mittel (Architektur-Verbesserung, Voraussetzung fuer flexiblen Provider-Wechsel) +**Herkunft:** Review des Lastenhefts "Vervollstaendigung der PostgreSQL-Implementierung", Punkt 6 +**Abhaengigkeit:** Setzt die abgeschlossene PostgreSQL-Paritaet voraus (Lastenheft_PostgreSQL_Implementation.md) +**Reihenfolge:** 3 von 5 + +### Umsetzungsreihenfolge aller Lastenhefte + +| Nr. | Lastenheft | Abhaengigkeit | +|-----|-----------|---------------| +| 1 | `Lastenheft_PostgreSQL_Implementation.md` | Keine | +| 2 | `Lastenheft_SQLite_ViewQuery_Bugfix.md` | Keine (unabhaengig, aber nach Nr. 1 geplant) | +| **3** | **`Lastenheft_IDbService_Interface.md`** (dieses Dokument) | Setzt Nr. 1 voraus | +| 4 | `Lastenheft_Statistik_View_Lesemethoden.md` | Setzt Nr. 1 voraus | +| 5 | `Lastenheft_MongoDB_Paritaet.md` | Keine direkte, logisch nach Nr. 1 | + +--- + +## Ausgangslage + +Nach Abschluss der PostgreSQL-Paritaet besitzen `SqliteDbService` und `PgSqlDbService` identische oeffentliche Methoden-Signaturen. Es fehlt jedoch ein formales Interface, das diese Parität abbildet. Aktuell referenziert der `ServiceContainer` beide Services als konkrete Typen. Ein Wechsel oder paralleler Betrieb erfordert manuelle Codeaenderungen an allen Aufrufstellen. + +### Aktuelle oeffentliche API beider Services (nach PgSQL-Paritaet) + +| Methode | Rueckgabe | +|---------|-----------| +| `InitializeDatabase()` | `void` | +| `SaveOrUpdateMachineAsync(Machine, bool)` | `Task` | +| `SaveHardwareInventoryAsync(int, HardwareInventory)` | `Task` | +| `SaveSoftwareInventoryAsync(int, SoftwareInventory)` | `Task` | +| `GetMachinesAsync()` | `Task>` | +| `GetAllActiveMachinesAsync()` | `Task>` | +| `GetAllActiveMachinesWithNetworkInfoAsync()` | `Task>` | +| `GetAllDeprovisionedMachinesAsync()` | `Task>` | +| `GetAllDisabledMachinesAsync()` | `Task>` | +| `GetMachineByIdAsync(int)` | `Task` | +| `GetMachineByNameAsync(string)` | `Task` | +| `GetLatestHardwareInventoryAsync(int)` | `Task` | +| `GetLatestSoftwareInventoryAsync(int)` | `Task` | +| `CleanupOldRecordsAsync(int)` | `Task` | +| `HasMachineRecordsAsync()` | `Task` | +| `HasHardwareInventoryRecordsAsync()` | `Task` | +| `HasSoftwareInventoryRecordsAsync()` | `Task` | +| `GetMachineCountAsync()` | `Task` | +| `GetHardwareInventoryCountAsync()` | `Task` | +| `GetSoftwareInventoryCountAsync()` | `Task` | +| `InitializeMachinesFromCsvAsync(string)` | `Task` | + +--- + +## Anforderungen + +### R-IDB-01: Interface-Definition + +Ein Interface `IDbService` wird unter `InventarWorkerCommon/Services/Database/` erstellt. Es enthaelt alle oeffentlichen Methoden, die sowohl `SqliteDbService` als auch `PgSqlDbService` anbieten (siehe Tabelle oben). + +### R-IDB-02: Implementierung durch beide Services + +- `SqliteDbService` implementiert `IDbService`. +- `PgSqlDbService` implementiert `IDbService`. +- Bestehende Funktionalitaet darf nicht veraendert werden. + +### R-IDB-03: Anpassung des ServiceContainers + +`ServiceContainer` exponiert die Datenbank-Services ueber das Interface: +- `DbService` wird zu `IDbService` (Property-Typ-Aenderung). +- `PgSqlDbService` wird zu `IDbService?` (nullable, da bei fehlendem Settings kein PgSQL-Service existiert). +- Der Konstruktor akzeptiert `IDbService` statt konkreter Typen. + +### R-IDB-04: Anpassung der Aufrufer + +Alle Stellen, die direkt `SqliteDbService` oder `PgSqlDbService` referenzieren, muessen auf `IDbService` umgestellt werden: +- `HarvesterWorkerService/Worker.cs` +- `InventarWorkerCommon/Services/Common/Initialize.cs` +- Weitere Aufrufer, die bei der Implementierung identifiziert werden. + +### R-IDB-05: XML-Dokumentation + +Das Interface `IDbService` muss vollstaendig mit XML-Kommentaren dokumentiert werden. Die Dokumentation der Methoden im Interface ist kanonisch; die implementierenden Klassen koennen via `` darauf verweisen. + +--- + +## Nicht im Scope + +- Dependency-Injection-Registrierung (z.B. ueber `IServiceCollection`). Die manuelle Instanziierung in `Initialize.cs` bleibt erhalten. +- Automatisches Provider-Switching ueber Konfiguration (z.B. via `appsettings.json`). Der konkrete Provider wird weiterhin in `Initialize.cs` gewaehlt. +- Erweiterung des Interfaces um MongoDB-Methoden (MongoDB hat eine andere API-Oberflaeche). + +--- + +## Akzeptanzkriterien + +| ID | Kriterium | +|----|-----------| +| AK-IDB-01 | `IDbService` existiert unter `InventarWorkerCommon/Services/Database/` mit allen Methoden aus der Tabelle oben. | +| AK-IDB-02 | `SqliteDbService` und `PgSqlDbService` implementieren `IDbService`. | +| AK-IDB-03 | `ServiceContainer` nutzt `IDbService` als Property-Typen. | +| AK-IDB-04 | Kein Consumer referenziert mehr direkt `SqliteDbService` oder `PgSqlDbService` (Ausnahme: `Initialize.cs` bei der Instanziierung). | +| AK-IDB-05 | Alle bestehenden Tests kompilieren und laufen unveraendert gruen. | +| AK-IDB-06 | `dotnet build` laeuft ohne Warnungen (bezogen auf den neuen Code) durch. | + +--- + +## Hinweis fuer Lernende + +**Deutsch:** Dieses Feature zeigt das "Program to an interface, not an implementation"-Prinzip (SOLID: Dependency Inversion). Durch ein gemeinsames Interface koennen verschiedene Datenbank-Provider ausgetauscht werden, ohne dass die aufrufende Logik geaendert werden muss. In der Praxis ermoeglicht das z.B. SQLite fuer lokale Entwicklung und PostgreSQL fuer Produktion -- der Anwendungscode bleibt identisch. + +**English:** This feature demonstrates the "Program to an interface, not an implementation" principle (SOLID: Dependency Inversion). A shared interface allows different database providers to be swapped without changing the calling logic. In practice, this enables e.g. SQLite for local development and PostgreSQL for production -- the application code remains identical. diff --git a/Lastenheft_MongoDB_Paritaet.md b/Lastenheft_MongoDB_Paritaet.md new file mode 100644 index 0000000..65613a2 --- /dev/null +++ b/Lastenheft_MongoDB_Paritaet.md @@ -0,0 +1,118 @@ +# Lastenheft: Herstellung der MongoDB-Paritaet zum SqliteDbService + +**Dokument-Status:** Entwurf +**Erstellt:** 2026-04-18 +**Betrifft:** `InventarWorkerCommon/Services/Database/MongoDbService.cs` +**Prioritaet:** Niedrig (Erweiterung, aktuelle Schreibfunktionalitaet ist nicht betroffen) +**Herkunft:** Review des Lastenhefts "Vervollstaendigung der PostgreSQL-Implementierung", Punkt 7 +**Reihenfolge:** 5 von 5 + +### Umsetzungsreihenfolge aller Lastenhefte + +| Nr. | Lastenheft | Abhaengigkeit | +|-----|-----------|---------------| +| 1 | `Lastenheft_PostgreSQL_Implementation.md` | Keine | +| 2 | `Lastenheft_SQLite_ViewQuery_Bugfix.md` | Keine (unabhaengig, aber nach Nr. 1 geplant) | +| 3 | `Lastenheft_IDbService_Interface.md` | Setzt Nr. 1 voraus | +| 4 | `Lastenheft_Statistik_View_Lesemethoden.md` | Setzt Nr. 1 voraus | +| **5** | **`Lastenheft_MongoDB_Paritaet.md`** (dieses Dokument) | Keine direkte, logisch nach Nr. 1 | + +--- + +## Ausgangslage + +Der `MongoDbService` verfuegt aktuell nur ueber drei Methoden: + +| Methode | Typ | +|---------|-----| +| `InitializeSoftwareMongoDatabase()` | Initialisierung | +| `InitializeHardwareMongoDatabase()` | Initialisierung | +| `SaveSoftwareInventoryAsync(int, SoftwareInventory)` | Schreiben | +| `SaveHardwareInventoryAsync(int, HardwareInventory)` | Schreiben | +| `FindSoftwareByNameAsync(int, string)` | Lesen (eingeschraenkt) | + +Im Vergleich zum `SqliteDbService` (und nach Abschluss der PgSQL-Paritaet auch zum `PgSqlDbService`) fehlen alle Lese-, Lookup-, Maintenance- und Statistik-Methoden. + +### Besonderheiten der MongoDB-Architektur + +MongoDB verwendet eine andere Datenstruktur als die relationalen Services: +- **Zwei separate Datenbanken:** `SoftwareInventory` und `HardwareInventory` +- **Collections pro Maschine:** Jede Maschine hat eine eigene Collection (benannt nach `machineId`) +- **Dokumentenbasiert:** Daten werden als BSON-Dokumente gespeichert, nicht in relationalen Tabellen +- **Kein `Machines`-Management:** MongoDB speichert keine Maschinenstammdaten -- diese kommen aus SQLite/PostgreSQL + +Diese Unterschiede bedeuten, dass nicht alle SQLite-Methoden 1:1 uebertragbar sind. Insbesondere Maschinen-CRUD und View-basierte Abfragen haben in MongoDB keine direkte Entsprechung. + +--- + +## Anforderungen + +### R-MONGO-01: Lesemethoden fuer Inventardaten + +- **R-MONGO-01.1: `GetLatestHardwareInventoryAsync(int machineId)`** + - Rueckgabe: Das neueste Hardware-Dokument der Collection `{machineId}` in der `HardwareInventory`-Datenbank. + - Sortierung nach `_id` oder `CreatedAt` absteigend, Limit 1. + +- **R-MONGO-01.2: `GetLatestSoftwareInventoryAsync(int machineId)`** + - Rueckgabe: Das neueste Software-Dokument der Collection `{machineId}` in der `SoftwareInventory`-Datenbank. + +- **R-MONGO-01.3: `FindHardwareByModelAsync(int machineId, string computerModel)`** + - Analog zu `FindSoftwareByNameAsync`, aber fuer Hardware-Dokumente nach Computermodell. + +### R-MONGO-02: Zaehler- und Existenz-Methoden + +- **R-MONGO-02.1: `GetHardwareInventoryCountAsync(int machineId)`** + - Rueckgabe: Anzahl der Hardware-Dokumente in der Collection des angegebenen Rechners. + +- **R-MONGO-02.2: `GetSoftwareInventoryCountAsync(int machineId)`** + - Rueckgabe: Anzahl der Software-Dokumente in der Collection des angegebenen Rechners. + +- **R-MONGO-02.3: `HasHardwareInventoryRecordsAsync(int machineId)`** + - Rueckgabe: `bool`, ob mindestens ein Hardware-Dokument existiert. + +- **R-MONGO-02.4: `HasSoftwareInventoryRecordsAsync(int machineId)`** + - Rueckgabe: `bool`, ob mindestens ein Software-Dokument existiert. + +**Hinweis:** Die Signatur weicht bewusst von SQLite ab (`machineId`-Parameter), da MongoDB Collections pro Maschine verwendet. Falls ein `IDbService`-Interface spaeter eingefuehrt wird, muss fuer MongoDB ein separates Interface oder eine Adapter-Schicht gewaehlt werden. + +### R-MONGO-03: Maintenance-Methode + +- **R-MONGO-03.1: `CleanupOldRecordsAsync(int machineId, int daysToKeep = 30)`** + - Loescht Dokumente aelter als `daysToKeep` Tage aus beiden Datenbanken fuer die angegebene `machineId`. + +### R-MONGO-04: Auflistung vorhandener Collections + +- **R-MONGO-04.1: `GetMachineIdsWithDataAsync()`** + - Rueckgabe: `Task>` -- Liste aller `machineId`-Werte, fuer die Collections existieren. + - Ermoeglicht die Abfrage, welche Maschinen ueberhaupt Inventardaten in MongoDB haben. + +--- + +## Nicht im Scope + +- Maschinen-CRUD in MongoDB (Stammdaten bleiben in SQLite/PostgreSQL). +- View-Aequivalente (MongoDB hat keine Views im relationalen Sinne; Aggregation Pipelines koennen spaeter hinzugefuegt werden). +- Einfuehrung eines gemeinsamen Interfaces mit den relationalen Services (siehe separates Lastenheft `IDbService`). +- Aenderung der bestehenden Collection-Struktur (eine Collection pro `machineId`). + +--- + +## Akzeptanzkriterien + +| ID | Kriterium | +|----|-----------| +| AK-MONGO-01 | Lesemethoden fuer Hardware- und Software-Inventar geben korrekte Dokumente zurueck. | +| AK-MONGO-02 | Zaehler- und Existenz-Methoden liefern korrekte Ergebnisse. | +| AK-MONGO-03 | `CleanupOldRecordsAsync` loescht ausschliesslich Dokumente aelter als die angegebene Schwelle. | +| AK-MONGO-04 | `GetMachineIdsWithDataAsync` gibt alle Collection-Namen zurueck, die gueltige `machineId`-Werte darstellen. | +| AK-MONGO-05 | Bestehende Schreibmethoden funktionieren unveraendert. | +| AK-MONGO-06 | Unit-Tests fuer alle neuen Methoden sind vorhanden und gruen. | +| AK-MONGO-07 | `dotnet build` laeuft ohne Warnungen (bezogen auf den neuen Code) durch. | + +--- + +## Hinweis fuer Lernende + +**Deutsch:** Dieses Feature zeigt die Unterschiede zwischen dokumentenbasierten (MongoDB) und relationalen (SQLite/PostgreSQL) Datenbanken. Waehrend relationale Services eine einheitliche Tabellen-API haben, erfordert MongoDB ein anderes Abfragemuster: Collections statt Tabellen, Filter-Builder statt SQL WHERE-Klauseln, und Dokument-Projektion statt SELECT-Spalten. "Paritaet" bedeutet hier nicht identische Signaturen, sondern aequivalente Funktionalitaet. + +**English:** This feature highlights the differences between document-based (MongoDB) and relational (SQLite/PostgreSQL) databases. While relational services share a uniform table API, MongoDB requires different query patterns: collections instead of tables, filter builders instead of SQL WHERE clauses, and document projection instead of SELECT columns. "Parity" here means equivalent functionality, not identical signatures. diff --git a/Lastenheft_PostgreSQL_Implementation.md b/Lastenheft_PostgreSQL_Implementation.md index c218621..354d43a 100644 --- a/Lastenheft_PostgreSQL_Implementation.md +++ b/Lastenheft_PostgreSQL_Implementation.md @@ -1,9 +1,21 @@ # Lastenheft: Vervollständigung der PostgreSQL-Implementierung (PGSQL-Parität) -**Dokument-Status:** Entwurf +**Dokument-Status:** Entwurf (Review-Entscheidungen eingearbeitet) **Erstellt:** 2026-04-12 +**Letzte Überarbeitung:** 2026-04-18 **Betrifft:** `InventarWorkerCommon/Services/Database/PgSqlDbService.cs`, `InventarWorkerCommon/Services/Common/Initialize.cs`, `HarvesterWorkerService/Worker.cs` **Priorität:** Hoch (Herstellung der funktionalen Parität zur SQLite-Implementierung) +**Reihenfolge:** 1 von 5 (dieses Lastenheft wird zuerst umgesetzt) + +### Umsetzungsreihenfolge aller Lastenhefte + +| Nr. | Lastenheft | Abhaengigkeit | +|-----|-----------|---------------| +| **1** | **`Lastenheft_PostgreSQL_Implementation.md`** (dieses Dokument) | Keine | +| 2 | `Lastenheft_SQLite_ViewQuery_Bugfix.md` | Keine (unabhaengig, aber nach Nr. 1 geplant) | +| 3 | `Lastenheft_IDbService_Interface.md` | Setzt Nr. 1 voraus | +| 4 | `Lastenheft_Statistik_View_Lesemethoden.md` | Setzt Nr. 1 voraus | +| 5 | `Lastenheft_MongoDB_Paritaet.md` | Keine direkte, logisch nach Nr. 1 | --- @@ -68,19 +80,20 @@ Abfrage der in `PgSqlDbService.cs` bereits definierten Views und Tabellen. - **Dapper:** Alle SQL-Abfragen müssen weiterhin via `Dapper` ausgeführt werden. - **Npgsql:** Nutzung von `NpgsqlConnection` für die Verbindung. -- **Async/Await:** Konsequente Nutzung von asynchronen Methoden (Parität zu SQLite). +- **Async/Await:** Konsequente Nutzung von asynchronen Methoden (Parität zu SQLite). **Ausnahme:** `InitializeDatabase()` bleibt synchron, da die Methode einmalig beim Startup aufgerufen wird und in SQLite ebenfalls synchron ist. - **DateTime-Handling:** PostgreSQL erwartet für `timestamptz` in der Regel UTC. Sicherstellen, dass `DateTime.UtcNow` verwendet wird. -- **View-Namen:** Die Namen der Views in PostgreSQL müssen exakt denen in SQLite entsprechen (PascalCase), um die Abfrageroutine identisch halten zu können. Bestehende Abweichungen (z.B. `hardware_inventory_view` vs. `HardwareInventoryView`) müssen korrigiert werden. +- **View-Namen:** Die Namen der Views in PostgreSQL müssen exakt denen in SQLite entsprechen (PascalCase), um die Abfrageroutine identisch halten zu können. Die bestehende Abweichung `hardware_inventory_view` muss im Rahmen dieses Features auf `HardwareInventoryView` umbenannt werden. Ebenso müssen die Spalten-Aliase dieser View an die SQLite-Konvention angeglichen werden (z.B. `machine_id` → `MachineID`, `machine_name` → `MachineName`). - **Dokumentation:** Alle neuen öffentlichen Methoden müssen vollständig mit XML-Kommentaren (zweisprachig oder konsistent zum Projekt) dokumentiert werden. -### R-PGSQL-07: Vorbereitung für Provider-Switching (Optional/Ausblick) +### R-PGSQL-07: Vorbereitung für Provider-Switching (Ausblick) -Obwohl dieses Lastenheft primär die Parität des Services betrifft, sollte die Implementierung so sauber sein, dass der `HarvesterWorkerService` in einem nächsten Schritt leicht auf PostgreSQL umgestellt oder beide parallel betrieben werden können. +Die Implementierung muss so sauber sein, dass der `HarvesterWorkerService` in einem späteren Schritt leicht auf PostgreSQL umgestellt oder beide parallel betrieben werden können. Ein formales `IDbService`-Interface ist **nicht** Teil dieses Vorhabens (siehe "Nicht im Scope"), die identischen Methoden-Signaturen bilden jedoch die Grundlage für ein späteres Interface-Refactoring. ### R-PGSQL-08: Konfigurations-Integration und Write-Safety - **R-PGSQL-08.1: Nutzung der Settings:** Der `PgSqlDbService` muss den über `Initialize.cs` bereitgestellten Connection-String (inkl. User/Passwort aus `PgSqlDb.PgSqlConnectionString`) für alle Datenbankverbindungen nutzen. -- **R-PGSQL-08.2: Berücksichtigung von `WriteEnabled`:** Im `HarvesterWorkerService` (oder an zentraler Stelle in `Initialize.cs`) muss sichergestellt werden, dass Schreibzugriffe auf PostgreSQL nur dann erfolgen, wenn `PgSqlDb.WriteEnabled` auf `true` gesetzt ist. Dies verhindert Fehlermeldungen bei unkonfigurierten PostgreSQL-Instanzen. +- **R-PGSQL-08.2: Berücksichtigung von `WriteEnabled`:** Im `HarvesterWorkerService` muss sichergestellt werden, dass Schreibzugriffe auf PostgreSQL nur dann erfolgen, wenn `PgSqlDb.WriteEnabled` auf `true` gesetzt ist. Dies verhindert Fehlermeldungen bei unkonfigurierten PostgreSQL-Instanzen. Die Worker-Integration erfolgt analog zu den bestehenden MongoDB-Aufrufen in `Worker.cs`. +- **R-PGSQL-08.3: Fallback-Pfad ohne Settings:** Wenn keine Settings-Datei vorhanden ist (parameterloser Aufruf von `Initialize.Services()`), wird die PostgreSQL-Initialisierung übersprungen. Der Connection-String ohne Credentials kann keine authentifizierte Verbindung aufbauen. `ServiceContainer.PgSqlDbService` darf in diesem Fall `null` sein; alle Aufrufer müssen darauf prüfen. --- @@ -89,6 +102,10 @@ Obwohl dieses Lastenheft primär die Parität des Services betrifft, sollte die - Migration bestehender SQLite-Daten nach PostgreSQL. - Performance-Optimierung (Indizes sind bereits im Schema-Script vorhanden). - Änderung der Domänenmodelle. +- Lesemethoden für Statistik-Views (`ComputerModelStatisticsView`, `ArchitectureStatisticsView`, `ModelArchitectureStatisticsView`, `HardwareStatisticsOverview`). Diese Views werden zwar beim Initialisieren erstellt, C#-Abfragemethoden sind jedoch weder in SQLite noch in PostgreSQL vorhanden und können als eigenes Feature nachgeliefert werden. +- Einführung eines formalen `IDbService`-Interfaces für Provider-Switching. Die identischen Methoden-Signaturen bereiten dies vor, das Interface-Refactoring ist ein eigenständiges Feature mit Auswirkung auf alle Consumer. +- Herstellung der MongoDB-Parität. Der `MongoDbService` besitzt aktuell nur Schreibmethoden und eine einzelne Abfrage. Die Erweiterung des MongoDB-Services ist nicht Gegenstand dieses Vorhabens. +- Behebung bestehender SQLite-Bugs (z.B. fehlerhafte View-Abfragen in `GetAllDeprovisionedMachinesAsync` und `GetAllDisabledMachinesAsync`). Diese werden in einem separaten Bug-Fix adressiert; die PgSQL-Implementierung verwendet von Anfang an die korrekten View-Abfragen. --- @@ -99,9 +116,12 @@ Obwohl dieses Lastenheft primär die Parität des Services betrifft, sollte die | AK-PGSQL-01 | `PgSqlDbService` besitzt alle öffentlichen Methoden des `SqliteDbService` mit identischen Signaturen. | | AK-PGSQL-02 | Alle Schreiboperationen speichern Daten korrekt in der PostgreSQL-Instanz (Verifikation via SQL-Abfrage). | | AK-PGSQL-03 | Der CSV-Import liest Daten erfolgreich in PostgreSQL ein. | -| AK-PGSQL-04 | Alle Methoden sind asynchron implementiert. | +| AK-PGSQL-04 | Alle Methoden sind asynchron implementiert (Ausnahme: `InitializeDatabase()` bleibt synchron, Parität zu SQLite). | | AK-PGSQL-05 | XML-Dokumentation für alle öffentlichen Member ist vorhanden. | | AK-PGSQL-06 | `dotnet build` läuft ohne Warnungen (bezogen auf den neuen Code) durch. | +| AK-PGSQL-07 | Der `HarvesterWorkerService` schreibt bei `WriteEnabled = true` parallel zu SQLite und MongoDB auch nach PostgreSQL. | +| AK-PGSQL-08 | Bei `WriteEnabled = false` oder fehlender Settings-Datei werden keine PostgreSQL-Schreibzugriffe ausgeführt und keine Exceptions geworfen. | +| AK-PGSQL-09 | Die View `HardwareInventoryView` in PostgreSQL verwendet PascalCase-Namen und -Spaltenaliase, identisch zur SQLite-Variante. | --- diff --git a/Lastenheft_SQLite_ViewQuery_Bugfix.md b/Lastenheft_SQLite_ViewQuery_Bugfix.md new file mode 100644 index 0000000..e6ec082 --- /dev/null +++ b/Lastenheft_SQLite_ViewQuery_Bugfix.md @@ -0,0 +1,83 @@ +# Lastenheft: Bugfix fehlerhafte View-Abfragen im SqliteDbService + +**Dokument-Status:** Entwurf +**Erstellt:** 2026-04-18 +**Betrifft:** `InventarWorkerCommon/Services/Database/SqliteDbService.cs` +**Prioritaet:** Mittel (funktionaler Bug, liefert aktuell ungefilterte Daten statt View-Ergebnisse) +**Herkunft:** Review des Lastenhefts "Vervollstaendigung der PostgreSQL-Implementierung", Punkt 1 +**Reihenfolge:** 2 von 5 + +### Umsetzungsreihenfolge aller Lastenhefte + +| Nr. | Lastenheft | Abhaengigkeit | +|-----|-----------|---------------| +| 1 | `Lastenheft_PostgreSQL_Implementation.md` | Keine | +| **2** | **`Lastenheft_SQLite_ViewQuery_Bugfix.md`** (dieses Dokument) | Keine (unabhaengig, aber nach Nr. 1 geplant) | +| 3 | `Lastenheft_IDbService_Interface.md` | Setzt Nr. 1 voraus | +| 4 | `Lastenheft_Statistik_View_Lesemethoden.md` | Setzt Nr. 1 voraus | +| 5 | `Lastenheft_MongoDB_Paritaet.md` | Keine direkte, logisch nach Nr. 1 | + +--- + +## Ausgangslage + +Im `SqliteDbService` enthalten zwei Methoden fehlerhaftes SQL. Statt die definierten Views `AllDeprovisionedMachinesView` und `AllDisabledMachinesView` abzufragen, wird die Tabelle `Machines` mit einem Alias gelesen. Das fuehrt dazu, dass die WHERE-Bedingungen der Views nicht greifen und alle Maschinen zurueckgegeben werden. + +| Methode | Zeile | Fehlerhaftes SQL | Erwartetes SQL | +|---------|-------|-----------------|---------------| +| `GetAllDeprovisionedMachinesAsync` | 496 | `SELECT * FROM Machines AllDeprovisionedMachinesView ORDER BY Name` | `SELECT * FROM AllDeprovisionedMachinesView ORDER BY Name` | +| `GetAllDisabledMachinesAsync` | 511 | `SELECT * FROM Machines AllDisabledMachinesView ORDER BY Name` | `SELECT * FROM AllDisabledMachinesView ORDER BY Name` | + +Zum Vergleich: Die Methoden `GetAllActiveMachinesAsync` (Zeile 464) und `GetAllActiveMachinesWithNetworkInfoAsync` (Zeile 482) verwenden korrekt die jeweilige View. + +--- + +## Anforderungen + +### R-SQLBUG-01: Korrektur der View-Abfrage in `GetAllDeprovisionedMachinesAsync` + +Die SQL-Abfrage in `SqliteDbService.GetAllDeprovisionedMachinesAsync()` muss korrigiert werden: + +- **Ist:** `SELECT * FROM Machines AllDeprovisionedMachinesView ORDER BY Name` +- **Soll:** `SELECT * FROM AllDeprovisionedMachinesView ORDER BY Name` + +### R-SQLBUG-02: Korrektur der View-Abfrage in `GetAllDisabledMachinesAsync` + +Die SQL-Abfrage in `SqliteDbService.GetAllDisabledMachinesAsync()` muss korrigiert werden: + +- **Ist:** `SELECT * FROM Machines AllDisabledMachinesView ORDER BY Name` +- **Soll:** `SELECT * FROM AllDisabledMachinesView ORDER BY Name` + +### R-SQLBUG-03: Testabdeckung + +Fuer beide korrigierten Methoden muessen Unit-Tests existieren, die sicherstellen, dass: +- Nur Maschinen mit `Disabled = 1 AND Deprovisioned = 1` von `GetAllDeprovisionedMachinesAsync` zurueckgegeben werden. +- Nur Maschinen mit `Disabled = 1 AND Deprovisioned = 0` von `GetAllDisabledMachinesAsync` zurueckgegeben werden. +- Maschinen mit abweichenden Flag-Kombinationen nicht im Ergebnis enthalten sind. + +--- + +## Nicht im Scope + +- Aenderungen an der PostgreSQL-Implementierung (wird separat im Lastenheft "PostgreSQL-Implementierung" behandelt). +- Aenderungen an den View-Definitionen selbst (diese sind korrekt). +- Refactoring oder Optimierung anderer Methoden im `SqliteDbService`. + +--- + +## Akzeptanzkriterien + +| ID | Kriterium | +|----|-----------| +| AK-SQLBUG-01 | `GetAllDeprovisionedMachinesAsync` gibt ausschliesslich Maschinen mit `Disabled = 1 AND Deprovisioned = 1` zurueck. | +| AK-SQLBUG-02 | `GetAllDisabledMachinesAsync` gibt ausschliesslich Maschinen mit `Disabled = 1 AND Deprovisioned = 0` zurueck. | +| AK-SQLBUG-03 | Unit-Tests fuer beide Methoden sind vorhanden und gruen. | +| AK-SQLBUG-04 | `dotnet build` laeuft ohne Warnungen durch. | + +--- + +## Hinweis fuer Lernende + +**Deutsch:** Dieser Bug zeigt ein haeufiges SQL-Muster: `SELECT * FROM TabelleA AliasB` ist syntaktisch korrekt und setzt einen Tabellen-Alias. Das Ergebnis ist jedoch voellig anders als `SELECT * FROM ViewB`. Solche Fehler fallen oft erst auf, wenn Filterbedingungen geprueft werden -- der Query laeuft fehlerfrei, liefert aber falsche Daten. + +**English:** This bug demonstrates a common SQL pattern: `SELECT * FROM TableA AliasB` is syntactically valid and sets a table alias. The result, however, is completely different from `SELECT * FROM ViewB`. Such errors often go unnoticed until filter conditions are verified -- the query runs without errors but returns incorrect data. diff --git a/Lastenheft_Statistik_View_Lesemethoden.md b/Lastenheft_Statistik_View_Lesemethoden.md new file mode 100644 index 0000000..1043c32 --- /dev/null +++ b/Lastenheft_Statistik_View_Lesemethoden.md @@ -0,0 +1,98 @@ +# Lastenheft: Lesemethoden fuer Statistik-Views + +**Dokument-Status:** Entwurf +**Erstellt:** 2026-04-18 +**Betrifft:** `InventarWorkerCommon/Services/Database/SqliteDbService.cs`, `InventarWorkerCommon/Services/Database/PgSqlDbService.cs` +**Prioritaet:** Niedrig (Erweiterung, keine bestehende Funktionalitaet betroffen) +**Herkunft:** Review des Lastenhefts "Vervollstaendigung der PostgreSQL-Implementierung", Punkt 3 +**Abhaengigkeit:** Setzt die abgeschlossene PostgreSQL-Paritaet voraus (Lastenheft_PostgreSQL_Implementation.md) +**Reihenfolge:** 4 von 5 + +### Umsetzungsreihenfolge aller Lastenhefte + +| Nr. | Lastenheft | Abhaengigkeit | +|-----|-----------|---------------| +| 1 | `Lastenheft_PostgreSQL_Implementation.md` | Keine | +| 2 | `Lastenheft_SQLite_ViewQuery_Bugfix.md` | Keine (unabhaengig, aber nach Nr. 1 geplant) | +| 3 | `Lastenheft_IDbService_Interface.md` | Setzt Nr. 1 voraus | +| **4** | **`Lastenheft_Statistik_View_Lesemethoden.md`** (dieses Dokument) | Setzt Nr. 1 voraus | +| 5 | `Lastenheft_MongoDB_Paritaet.md` | Keine direkte, logisch nach Nr. 1 | + +--- + +## Ausgangslage + +Sowohl der `SqliteDbService` als auch der `PgSqlDbService` erstellen beim Initialisieren mehrere Statistik-Views: + +| View | Inhalt | +|------|--------| +| `ComputerModelStatisticsView` | Verteilung nach Computermodell mit Anzahl, Prozentsatz, Zeitraum | +| `ArchitectureStatisticsView` | Verteilung nach Architektur mit Kernen, Speicher, Zeitraum | +| `ModelArchitectureStatisticsView` | Kombinierte Verteilung Modell x Architektur | +| `HardwareStatisticsOverview` | Vereinigte Uebersicht (Modell + Architektur als Kategorien) | + +Diese Views existieren in der Datenbank, aber es gibt **keine C#-Methoden**, um sie abzufragen. Die Daten sind derzeit nur ueber direkten SQL-Zugriff erreichbar. + +--- + +## Anforderungen + +### R-STAT-01: Lesemethode fuer ComputerModelStatisticsView + +- **Methode:** `GetComputerModelStatisticsAsync()` +- **Rueckgabe:** `Task>` +- **Neues Modell:** `ComputerModelStatistic` mit Properties: `ComputerModel`, `AnzahlMaschinen`, `EinzigartigeMaschinen`, `Prozentsatz`, `ErsteErfassung`, `LetzteErfassung` + +### R-STAT-02: Lesemethode fuer ArchitectureStatisticsView + +- **Methode:** `GetArchitectureStatisticsAsync()` +- **Rueckgabe:** `Task>` +- **Neues Modell:** `ArchitectureStatistic` mit Properties: `Architecture`, `AnzahlMaschinen`, `EinzigartigeMaschinen`, `Prozentsatz`, `DurchschnittlicheKerne`, `DurchschnittlicherSpeicherGB`, `ErsteErfassung`, `LetzteErfassung` + +### R-STAT-03: Lesemethode fuer ModelArchitectureStatisticsView + +- **Methode:** `GetModelArchitectureStatisticsAsync()` +- **Rueckgabe:** `Task>` +- **Neues Modell:** `ModelArchitectureStatistic` mit Properties: `ComputerModel`, `Architecture`, `AnzahlMaschinen`, `EinzigartigeMaschinen`, `Prozentsatz`, `DurchschnittlicheKerne`, `DurchschnittlicherSpeicherGB`, `ErsteErfassung`, `LetzteErfassung` + +### R-STAT-04: Lesemethode fuer HardwareStatisticsOverview + +- **Methode:** `GetHardwareStatisticsOverviewAsync()` +- **Rueckgabe:** `Task>` +- **Neues Modell:** `HardwareStatisticOverview` mit Properties: `Kategorie`, `Wert`, `Anzahl`, `Prozentsatz` + +### R-STAT-05: Provider-Paritaet + +Alle vier Methoden muessen sowohl im `SqliteDbService` als auch im `PgSqlDbService` mit identischen Signaturen implementiert werden. + +### R-STAT-06: Modelle + +Die neuen Modellklassen werden unter `InventarWorkerCommon/Models/SqlDatabase/` abgelegt. Property-Namen entsprechen den Spaltenaliases der Views. + +--- + +## Nicht im Scope + +- Aenderung der View-Definitionen (SQL bleibt unveraendert). +- REST-API-Endpunkte fuer Statistiken (kann als separates Feature folgen). +- Aenderung bestehender Methoden oder Modelle. + +--- + +## Akzeptanzkriterien + +| ID | Kriterium | +|----|-----------| +| AK-STAT-01 | Vier neue Lesemethoden sind in `SqliteDbService` und `PgSqlDbService` vorhanden. | +| AK-STAT-02 | Alle Methoden geben typisierte Modell-Listen zurueck (kein `dynamic` oder `BsonDocument`). | +| AK-STAT-03 | Vier neue Modellklassen unter `Models/SqlDatabase/` existieren mit vollstaendiger XML-Dokumentation. | +| AK-STAT-04 | Unit-Tests fuer alle Lesemethoden sind vorhanden und gruen. | +| AK-STAT-05 | `dotnet build` laeuft ohne Warnungen (bezogen auf den neuen Code) durch. | + +--- + +## Hinweis fuer Lernende + +**Deutsch:** Dieses Feature zeigt, wie bestehende Datenbank-Views ueber typisierte C#-Modelle zugaenglich gemacht werden. Der Vorteil gegenueber rohen SQL-Abfragen: IntelliSense, Compile-Time-Checks und einfachere Testbarkeit. Die Views selbst bleiben unveraendert -- nur die "Bruecke" zwischen Datenbank und Anwendungscode wird gebaut. + +**English:** This feature demonstrates how existing database views are made accessible through typed C# models. The advantage over raw SQL queries: IntelliSense, compile-time checks, and easier testability. The views themselves remain unchanged -- only the "bridge" between database and application code is built. diff --git a/specs/001-pgsql-paritaet/checklists/plan.md b/specs/001-pgsql-paritaet/checklists/plan.md new file mode 100644 index 0000000..c137976 --- /dev/null +++ b/specs/001-pgsql-paritaet/checklists/plan.md @@ -0,0 +1,267 @@ +# Plan Quality Checklist: PostgreSQL-Parität zum SqliteDbService + +**Purpose**: Validate completeness, clarity, consistency, and implementability of plan.md + and related design artifacts before task creation and implementation start +**Created**: 2026-04-18 +**Feature**: [plan.md](../plan.md) | [spec.md](../spec.md) | [research.md](../research.md) + | [data-model.md](../data-model.md) | [contracts/](../contracts/) | [quickstart.md](../quickstart.md) + +--- + +## Plan-Vollständigkeit / Plan Completeness + +- [x] CHK001 Sind alle Pflichtabschnitte des Plan-Templates (Summary, Technical Context, + Constitution Check, Project Structure) vollständig ausgefüllt und frei von + ungefüllten Platzhaltern? [Completeness, plan.md] + > **Befund**: Keine Platzhalter-Tokens. Alle Pflichtabschnitte (Summary, Technical Context, + > Constitution Check, Project Structure, Implementation Notes) haben konkreten Inhalt. ✅ + +- [x] CHK002 Sind alle vier Phase-1-Artefakte (research.md, data-model.md, contracts/, + quickstart.md) im specs-Verzeichnis tatsächlich vorhanden und nicht leer? [Completeness] + > **Befund**: research.md ✅, data-model.md ✅, contracts/PgSqlDbService-methods.md ✅, + > quickstart.md ✅. Alle vorhanden und mit Inhalt gefüllt. + +- [x] CHK003 Enthält plan.md §Implementation Notes konkrete, umsetzungsreife Hinweise für alle + vier kritischen technischen Entscheidungen (RETURNING Id, DateTime.UtcNow, DROP VIEW, + Null-Pattern)? [Completeness, plan.md §Implementation Notes] + > **Befund**: Alle vier Punkte + CSV-Import (5 Punkte gesamt) vorhanden: + > (1) RETURNING Id + QuerySingleAsync, (2) DateTime.UtcNow, (3) DROP VIEW IF EXISTS, + > (4) Null-Pattern + Konstruktor + Dispose-null-Check (ergänzt), (5) CSV await using. ✅ + +--- + +## Technischer Kontext / Technical Context Clarity + +- [x] CHK004 Sind die Performance-Ziele für Schreiboperationen quantifiziert, nicht nur für + Leseoperationen ("< 500 ms für Listen-Abfragen")? [Clarity, plan.md §Technical Context, Gap] + > **Befund**: Schreib-Durchsatz bewusst nicht absolut quantifiziert — als akzeptierte Unschärfe + > für MVP-Scope mit Single-Writer und ~200 Maschinen dokumentiert. plan.md §Technical Context + > ergänzt um explizite Erläuterung. [Accepted Gap — documented 2026-04-18] ✅ + +- [x] CHK005 Ist der Vergleichsmaßstab "vergleichbar mit SQLite" für den Schreib-Durchsatz mit + einer konkreten Referenzmessung hinterlegt? [Ambiguity, plan.md §Technical Context] + > **Befund**: Keine gespeicherte SQLite-Basismessung — "vergleichbar" ist definiert als + > "async Dapper-Schreibzugriff auf lokaler PostgreSQL-Instanz unter gleicher Last". Als + > bewusst ungebundene Referenz in plan.md §Technical Context dokumentiert. [Accepted Ambiguity] ✅ + +- [x] CHK006 Sind die PostgreSQL-Mindestversion (14+) und ihre Begründung (GENERATED BY DEFAULT + AS IDENTITY, RETURNING Id, timestamptz) konsistent in plan.md §Constraints, spec.md + §Assumptions und data-model.md dokumentiert? [Consistency] + > **Befund**: plan.md §Constraints ✅, spec.md §Assumptions ✅. data-model.md fehlte — ergänzt + > mit Hinweis auf PostgreSQL 14+ und den drei Grund-Features am Dateianfang. Konsistent. ✅ + +--- + +## Constitution-Gates-Qualität / Constitution Gates Quality + +- [x] CHK007 Sind alle neun Constitution-Gates in plan.md §Constitution Check mit spezifischer + Evidenz belegt (z.B. Verweis auf FR-017, CA-007, konkrete Paketnamen) und nicht nur + mit einem Häkchen ohne Begründung? [Completeness, plan.md §Constitution Check] + > **Befund**: Alle 9 Gates haben konkrete Referenzen: Branching (Branch-Name), Toolchain + > (.NET 10, LangVersion 14.0), Architecture (Dateipfade), Documentation (FR-017, CA-007), + > XML/DocFX (docfx.json), Testing (Unit+Integration, CI-Gate), Dependency (Paketnamen+Versionen), + > Data Contract (System.Text.Json, FR-013), Statistical (docs/project-statistics.md). ✅ + +- [x] CHK008 Wird für den Dependency-Currency-Gate in plan.md dokumentiert, dass + `dotnet list package --outdated` als konkreter Polish-Task geplant ist? [Completeness, + plan.md §Constitution Check — Dependency Currency Gate] + > **Befund**: plan.md §Dependency Currency Gate enthält explizit: "`dotnet list package + > --outdated` wird als Polish-Task ausgeführt." Und quickstart.md §Schritt 9 listet + > den Befehl als Abschluss-Validierungsschritt. ✅ + +- [x] CHK009 Beschreibt der Testing/Coverage-Gate, welche Kombination aus Unit-Tests und + Integrationstests konkret für ≥70% Abdeckung der 21 neuen Methoden sorgen soll? + [Completeness, plan.md §Constitution Check — Testing/Coverage Gate] + > **Befund**: Testing/Coverage-Gate nennt explizit "Unit-Tests (Logik, null-Checks) + + > Integrationstests (full method coverage)". Beide Typen als notwendig dokumentiert. ✅ + +--- + +## Forschungsentscheidungen / Research Decisions Quality + +- [x] CHK010 Ist das Verhalten für negative `daysToKeep`-Werte (z.B. -5, was zu einem + Cutoff-Datum in der Zukunft führt) in research.md §R-07 oder spec.md §Edge Cases + spezifiziert oder bewusst ausgeklammert? [Coverage, research.md §R-07, Gap] + > **Befund**: research.md §R-07 ergänzt: daysToKeep=-5 → cutoff=jetzt+5 → DELETE löscht + > alle Einträge (mathematisch identisch mit daysToKeep=0). Deterministische Semantik, + > kein Sonderfall, kein Fix nötig. Dokumentiert 2026-04-18. ✅ + +- [x] CHK011 Enthält research.md §R-08 eine klare Zuordnung, welche der 21 Methoden durch + Unit-Tests ohne echte PostgreSQL-Instanz abgedeckt werden können und welche + Integrationstests erfordern? [Completeness, research.md §R-08] + > **Befund**: research.md §R-08 ergänzt um explizite Trennlinie: Unit-testbar (null-Checks, + > ArgumentNullException, FileNotFoundException, Cutoff-Berechnung, WriteEnabled-Fallback); + > Integration erforderlich (alle 21 DB-Methoden die SQL ausführen). ✅ + +- [x] CHK012 Sind alle zehn Entscheidungen in research.md mit einem "Alternatives considered"- + Abschnitt versehen, der mindestens zwei verworfene Alternativen nennt? [Completeness, + research.md] + > **Befund**: R-01 bis R-09 und R-11 hatten bereits "Alternatives considered". R-10 fehlte — + > ergänzt mit 3 verworfenen Alternativen (2026-04-18). Alle 11 Entscheidungen vollständig. ✅ + +--- + +## Datenmodell-Qualität / Data Model Quality + +- [x] CHK013 Erklärt data-model.md den semantischen Unterschied zwischen der alten + `hardware_inventory_view` (GROUP BY m.Name ohne CreatedAt-Sortierung) und der neuen + `HardwareInventoryView` (DISTINCT ON m.id ORDER BY h.CreatedAt DESC), der mehr als nur + eine Umbenennung ist? [Completeness, data-model.md §HardwareInventoryView] + > **Befund**: data-model.md §HardwareInventoryView ergänzt um Erklärung der semantischen + > Änderung: altes ORDER BY m.name ASC → nicht-deterministischer Eintrag; neues + > ORDER BY h.CreatedAt DESC → immer neuester Eintrag. Semantische Korrektur dokumentiert. ✅ + +- [x] CHK014 Spezifiziert data-model.md das ON DELETE-Verhalten für die Fremdschlüssel + `HardwareInventories.MachineId → Machines.Id` und `SoftwareInventories.MachineId + → Machines.Id`? [Completeness, data-model.md §HardwareInventories, §SoftwareInventories, Gap] + > **Befund**: data-model.md ergänzt: kein explizites ON DELETE-Constraint → + > PostgreSQL-Standard RESTRICT (sicheres Default). Entscheidung dokumentiert: RESTRICT + > schützt vor versehentlichem Maschinen-Löschen. Konsistent mit PgSqlDbService.cs. ✅ + +- [x] CHK015 Ist das Typ-Mapping für `Disabled` und `Deprovisioned` zwischen PostgreSQL, + C#-Modell `Machine`, C#-Modell `MachineFromCsv` und CsvHelper konsistent dokumentiert? + [Consistency, data-model.md §Machine — RESOLVED 2026-04-18] + > **Befund / Finding**: Machine.cs, MachineState.cs, MachineFromCsv.cs verwenden alle `bool`. + > PostgreSQL-Spalte auf `BOOLEAN NOT NULL DEFAULT FALSE` geändert (war INTEGER). + > View-WHERE-Klauseln auf `= FALSE`/`= TRUE` aktualisiert. CsvHelper konvertiert + > `"0"` → false / `"1"` → true nativ. data-model.md §Typ-Hinweise dokumentiert dies. + +- [x] CHK016 Dokumentiert data-model.md §Views explizit, welche der neun Views in diesem + Feature C# Read-Methoden erhalten und welche nur als SQL-Schema angelegt, aber ohne + Read-Methoden in diesem Feature geliefert werden? [Completeness, data-model.md §Views, Gap] + > **Befund**: data-model.md §Views ergänzt um §C#-Methoden-Abdeckung: 4 Views mit C#-Methode + > (Active, ActiveWithNetworkInfo, Disabled, Deprovisioned) vs. 5 Views ohne (Hardware...View, + > Latest..., 4 Statistik-Views). Statistik-Views explizit als deferred markiert. ✅ + +--- + +## Methodenverträge / Method Contracts Quality + +- [x] CHK017 Spezifiziert contracts/PgSqlDbService-methods.md das ID-Handling für + PgSQL-FK-Schreibzugriffe eindeutig? [Clarity, contracts/ §Worker.cs — RESOLVED 2026-04-18] + > **Entscheidung / Decision**: SQLite ist die führende DB. Der Worker setzt + > `machine.Id = _machineId` (SQLite-Id) vor dem PgSQL-Aufruf. `PgSqlDbService.SaveOrUpdateMachineAsync` + > führt einen INSERT mit dieser expliziten Id durch (`GENERATED BY DEFAULT AS IDENTITY`). + > Damit kann `_machineId` sicher für alle FK-Writes in PostgreSQL verwendet werden. + > Dokumentiert in research.md §R-11, plan.md §Worker.cs-Pattern, contracts/ §Worker.cs. + +- [x] CHK018 Sind Exception-Typen für alle Schreib-Methoden in contracts/ dokumentiert + (nicht nur für `SaveOrUpdateMachineAsync`)? [Completeness, contracts/, Gap] + > **Befund**: contracts/ ergänzt: SaveHardwareInventoryAsync, SaveSoftwareInventoryAsync, + > CleanupOldRecordsAsync erhalten jeweils `NpgsqlException` in Throws-Abschnitt. + > InitializeMachinesFromCsvAsync hatte bereits FileNotFoundException + Exception; + > NpgsqlException ergänzt. ✅ + +- [x] CHK019 Beschreibt contracts/ das asynchrone Disposal-Muster (`await using`) für + `NpgsqlConnection` und `NpgsqlTransaction` in `InitializeMachinesFromCsvAsync`? [Completeness, + contracts/ §InitializeMachinesFromCsvAsync] + > **Befund**: contracts/ ergänzt mit explizitem `await using`-Pattern inkl. Code-Beispiel: + > `await using var connection = new NpgsqlConnection(...); await using var transaction = ...` + > Begründung (IAsyncDisposable) und Parität zu research.md §R-09 dokumentiert. ✅ + +- [x] CHK020 Dokumentiert contracts/ §ServiceContainer-Änderungen vollständig alle drei + notwendigen Code-Änderungen: (1) Property wird nullable, (2) Konstruktor entfernt + ArgumentNullException, (3) Dispose-Methoden erhalten null-Check? [Completeness, + contracts/ §ServiceContainer-Änderungen] + > **Befund**: contracts/ §ServiceContainer-Änderungen überarbeitet: alle drei Punkte + > einzeln mit Vorher/Nachher-Code-Beispielen dokumentiert inkl. Dispose-Pattern für + > Sync und Async. Abgeglichen mit Initialize.cs (Ist-Zustand). ✅ + +--- + +## Teststrategie-Qualität / Test Strategy Quality + +- [x] CHK021 Definiert plan.md §Implementation Notes (oder research.md §R-08) eine + vollständige Liste der Unit-Test-Szenarien, die ausreichend für ≥70% Abdeckung + der neuen Code-Pfade in PgSqlDbService.cs sind? [Completeness, plan.md §Test-Strategie] + > **Befund**: 5 Unit-Tests allein reichen nicht für ≥70% auf ~800 Zeilen DB-Code — Gap + > explizit dokumentiert in plan.md §Test-Strategie (Coverage-Hinweis). Coverage-Gate + > ≥70% gilt für kombinierten Lauf mit Integration-Tests. Akzeptierter Gap. ✅ + +- [x] CHK022 Spezifiziert der Testplan den Mechanismus, mit dem Integrationstests selektiv + aus normalen CI-Läufen ausgeschlossen werden (z.B. `[TestCategory("Integration")]` + + `--filter TestCategory!=Integration`)? [Completeness, plan.md §Test-Strategie, Gap] + > **Befund**: plan.md §Test-Strategie ergänzt: `dotnet test --filter "TestCategory!=Integration"` + > für normalen CI-Lauf und `dotnet test --filter "TestCategory=Integration"` für Integrationstest- + > Lauf. Beide Befehle explizit dokumentiert. ✅ + +- [x] CHK023 Ist die Umgebungsvariable `PGSQL_TEST_CONNECTION_STRING` in allen drei + relevanten Dokumenten (plan.md, research.md, quickstart.md) konsistent benannt + und beschrieben? [Consistency] + > **Befund**: quickstart.md §Schritt 1 verwendete `TestRunParameters` statt Env-Variable — + > vereinheitlicht auf `PGSQL_TEST_CONNECTION_STRING` (export) + `--filter "TestCategory=Integration"`. + > Konsistent mit plan.md und research.md §R-08. Außerdem `Database=inventar_test` in quickstart. ✅ + +- [x] CHK024 Definiert der Testplan Teardown-Anforderungen für Integrationstests — insbesondere + die Verwendung einer dedizierten Test-Datenbank und deren Bereinigung nach dem Testlauf? + [Coverage, plan.md §Test-Strategie, Gap] + > **Befund**: plan.md §Test-Strategie ergänzt: `inventar_test` als dedizierte Test-DB; + > ClassCleanup mit TRUNCATE ... RESTART IDENTITY CASCADE. quickstart.md ergänzt um + > entsprechenden Hinweis. ✅ + +--- + +## Implementierungshinweise / Implementation Notes Quality + +- [x] CHK025 Ist der Worker.cs-Code-Block in plan.md §Implementation Notes klar und + widerspruchsfrei dokumentiert? [Clarity, plan.md §Implementation Notes — RESOLVED 2026-04-18] + > **Entscheidung / Decision**: `_machineId` (SQLite-Id) ist die kanonische Id. Der Worker + > setzt `machine.Id = _machineId` vor dem PgSQL-Aufruf. `PgSqlDbService.SaveOrUpdateMachineAsync` + > übernimmt diese Id via explizitem INSERT. Kein separates `pgMachineId` nötig. + > Dokumentiert in research.md §R-11, plan.md §Worker.cs-Pattern, contracts/ §Worker.cs. + +- [x] CHK026 Beschreibt plan.md §Implementation Notes den vollständigen Lifecycle der + nullable `PgSqlDbService?`-Instanz in allen vier relevanten Aspekten: Initialisierung, + null-Rückgabe, null-Checks im Worker, Dispose? [Completeness, plan.md §Implementation Notes] + > **Befund**: plan.md §Null-Pattern (Punkt 4) ergänzt um Konstruktor-Änderung (ArgumentNull + > entfernen) und Dispose/DisposeAsync null-Check. Alle 4 Aspekte aus research.md §R-03 + > sind jetzt in plan.md §Implementation Notes abgebildet. ✅ + +--- + +## Quickstart-Qualität / Quickstart Quality + +- [x] CHK027 Enthält quickstart.md §Schritt 7 (WriteEnabled=false) eine SQL-Negativverifikation, + die beweist, dass nach dem Worker-Start tatsächlich KEINE Datensätze in PostgreSQL + gespeichert wurden? [Completeness, quickstart.md §Schritt 7, Gap] + > **Befund**: quickstart.md §Schritt 7 ergänzt um SQL-Block mit SELECT COUNT(*) für alle + > drei Tabellen (expected: 0) und Erklärung was Wert > 0 bedeutet (Guard greift nicht). ✅ + +- [x] CHK028 Stimmt das CSV-Spaltenformat in quickstart.md §Schritt 4 mit dem CsvHelper- + Mapping in `MachineMapFromCsv` überein? [Consistency, quickstart.md §Schritt 4 — + RESOLVED 2026-04-18] + > **Befund / Finding**: Verifiziert — CsvHelper `BooleanConverter` konvertiert `"0"` → false + > und `"1"` → true für `bool`-Properties nativ. `MachineMapFromCsv` benötigt keine + > explizite Konversion. quickstart.md §Schritt 4 ergänzt um Hinweis auf + > bool-Konvertierung und BOOLEAN-Datenbanktyp. + +--- + +## Artefakt-Konsistenz / Cross-Document Consistency + +- [x] CHK029 Sind die 21 in contracts/PgSqlDbService-methods.md dokumentierten Methoden + vollständig deckungsgleich mit den 21 SqliteDbService-Methoden (Signaturen und + Rückgabetypen identisch), wie in spec.md §SC-001 gefordert? [Consistency, + contracts/, spec.md §SC-001] + > **Befund**: Alle 20 neuen Methoden (+ InitializeDatabase = 21) geprüft. Methodennamen, + > Parameter und Rückgabetypen identisch zu SqliteDbService. Einzige Abweichung: + > SaveOrUpdateMachineAsync verlangt machine.Id > 0 (verhaltensbedingt, nicht Signatur). + > Kein Signatur-Konflikt. ✅ + +- [x] CHK030 Ist die Aussage "C# Read-Methoden für Statistik-Views sind nicht Teil dieses + Features" konsistent in spec.md §Assumptions, research.md §R-10, data-model.md §Views + und contracts/ dokumentiert — ohne Widersprüche? [Consistency] + > **Befund**: spec.md §Assumptions ✅, research.md §R-10 ✅, data-model.md §Views (ergänzt + > mit C#-Methoden-Abdeckung) ✅, contracts/ (neuer §Deferred-Abschnitt) ✅. Kein Widerspruch + > gefunden. Konsistent in allen 4 Dokumenten. ✅ + +--- + +## Notizen / Notes + +- `[Gap]` = Anforderung fehlt oder ist nicht dokumentiert +- `[Ambiguity]` = Anforderung ist mehrdeutig oder nicht quantifiziert +- `[Conflict]` = Zwei Anforderungen widersprechen sich +- `[Assumption]` = Aussage basiert auf einer unverifizierten Annahme +- Abgehakte Punkte: `[x]` statt `[ ]` +- Kritische Befunde (insb. CHK017, CHK025) sollten vor `/speckit.tasks` behoben werden diff --git a/specs/001-pgsql-paritaet/checklists/requirements.md b/specs/001-pgsql-paritaet/checklists/requirements.md new file mode 100644 index 0000000..d05a1c2 --- /dev/null +++ b/specs/001-pgsql-paritaet/checklists/requirements.md @@ -0,0 +1,36 @@ +# Specification Quality Checklist: PostgreSQL-Parität zum SqliteDbService + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-04-18 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Spec ist vollständig und ready for `/speckit.plan`. +- Alle Review-Entscheidungen aus dem Lastenheft-Review (8 Punkte) sind in den Assumptions und FR dokumentiert. +- US1 (Schreiben) ist MVP — alle weiteren Stories bauen darauf auf. diff --git a/specs/001-pgsql-paritaet/contracts/PgSqlDbService-methods.md b/specs/001-pgsql-paritaet/contracts/PgSqlDbService-methods.md new file mode 100644 index 0000000..a77b173 --- /dev/null +++ b/specs/001-pgsql-paritaet/contracts/PgSqlDbService-methods.md @@ -0,0 +1,468 @@ +# Service Method Contracts: PgSqlDbService + +**Branch**: `001-pgsql-paritaet` | **Date**: 2026-04-18 + +**Namespace**: `InventarWorkerCommon.Services.Database` +**Class**: `PgSqlDbService` +**File**: `InventarWorkerCommon/Services/Database/PgSqlDbService.cs` + +Diese Datei dokumentiert alle öffentlichen Methoden des `PgSqlDbService` nach Implementierung. +Die Signaturen sind identisch zu `SqliteDbService` (Parität), sofern nicht anders vermerkt. + +--- + +## Bestehende Methoden / Existing Methods (nach diesem Feature unverändert / unchanged) + +### InitializeDatabase() + +```csharp +public void InitializeDatabase() +``` + +**Beschreibung / Description**: +DE: Erstellt Tabellen, Indizes und Views; stellt Verbindung zur PostgreSQL-Datenbank her. + Nach diesem Feature: führt zusätzlich `DROP VIEW IF EXISTS hardware_inventory_view` aus (FR-013). +EN: Creates tables, indexes, and views; establishes the PostgreSQL connection. + After this feature: additionally executes `DROP VIEW IF EXISTS hardware_inventory_view` (FR-013). + +**Throws**: `NpgsqlException` wenn PostgreSQL nicht erreichbar (Fail-hard, FR-014). +**Returns**: `void` (synchron / synchronous — Parität zu SqliteDbService). + +--- + +## Neue Methoden / New Methods (21 Methoden / 21 methods — Parität zu SqliteDbService) + +### Schreiben / Write Methods + +#### SaveOrUpdateMachineAsync + +```csharp +public async Task SaveOrUpdateMachineAsync(Machine machine, bool isHarvester = false) +``` + +**Beschreibung / Description**: +DE: Speichert eine neue Maschine oder aktualisiert einen bestehenden Datensatz anhand des Namens. + Bei `isHarvester = true` werden zusätzlich IPv4, IPv6, FQDN und LastHarvested aktualisiert. + Gibt die Id der Maschine zurück. Nutzt `RETURNING Id` statt `last_insert_rowid()`. +EN: Saves a new machine or updates an existing record by name. + With `isHarvester = true`, also updates IPv4, IPv6, FQDN, and LastHarvested. + Returns the machine's Id. Uses `RETURNING Id` instead of `last_insert_rowid()`. + +**Parameters**: +- `machine`: `Machine` — Maschinendaten / Machine data (must not be null; `machine.Id` MUSS auf die SQLite-Id gesetzt sein — der Aufrufer setzt `machine.Id = sqliteMachineId` vor dem Aufruf) +- `isHarvester`: `bool` — Erweitertes Update / Extended update flag (default: false) + +**Returns**: `Task` — Id der Maschine in PostgreSQL / Machine Id in PostgreSQL (=`machine.Id`, da PostgreSQL die SQLite-Id übernimmt). + +**Throws**: +- `ArgumentNullException` wenn `machine` null. +- `ArgumentException` wenn `machine.Id == 0` (Id wurde nicht aus SQLite übernommen). +- `NpgsqlException` bei Verbindungsfehler oder PostgreSQL-Fehler (propagiert ungefangen). + +**Architekturhinweis / Architecture Note**: +DE: PostgreSQL ist eine optionale Senke, SQLite ist die führende Datenbank. Diese Methode + verwendet `machine.Id` für den INSERT, damit IDs über beide Datenbanken konsistent bleiben. + `GENERATED BY DEFAULT AS IDENTITY` erlaubt explizite Id-Werte. +EN: PostgreSQL is an optional sink; SQLite is the authoritative database. This method uses + `machine.Id` for the INSERT so that Ids stay consistent across both databases. + `GENERATED BY DEFAULT AS IDENTITY` permits explicit Id values. + +--- + +#### SaveHardwareInventoryAsync + +```csharp +public async Task SaveHardwareInventoryAsync(int machineId, HardwareInventory hardware) +``` + +**Beschreibung / Description**: +DE: Speichert eine Hardware-Momentaufnahme für die angegebene Maschine in `HardwareInventories`. +EN: Saves a hardware snapshot for the specified machine in `HardwareInventories`. + +**Parameters**: +- `machineId`: `int` — Id der Maschine / Machine Id +- `hardware`: `HardwareInventory` — Hardware-Daten / Hardware data + +**Returns**: `Task` (void async). + +**Throws**: `NpgsqlException` bei Verbindungsfehler oder PostgreSQL-Fehler (propagiert ungefangen). + +--- + +#### SaveSoftwareInventoryAsync + +```csharp +public async Task SaveSoftwareInventoryAsync(int machineId, SoftwareInventory software) +``` + +**Beschreibung / Description**: +DE: Serialisiert alle Teillisten des SoftwareInventory-Objekts nach JSON und speichert sie + in `SoftwareInventories`. Nutzt `System.Text.Json`. +EN: Serializes all sub-lists of the SoftwareInventory object to JSON and stores them + in `SoftwareInventories`. Uses `System.Text.Json`. + +**Parameters**: +- `machineId`: `int` — Id der Maschine / Machine Id +- `software`: `SoftwareInventory` — Software-Daten / Software data + +**Returns**: `Task` (void async). + +**Throws**: `NpgsqlException` bei Verbindungsfehler oder PostgreSQL-Fehler (propagiert ungefangen). + +--- + +### Lesen / Read Methods — Machine-Listen / Machine Lists + +#### GetMachinesAsync + +```csharp +public async Task> GetMachinesAsync() +``` + +**Beschreibung / Description**: +DE: Gibt alle Maschinen aus der `Machines`-Tabelle zurück, sortiert nach Name. +EN: Returns all machines from the `Machines` table, ordered by name. + +**Returns**: `Task>`. + +--- + +#### GetAllActiveMachinesAsync + +```csharp +public async Task> GetAllActiveMachinesAsync() +``` + +**Beschreibung / Description**: +DE: Gibt alle aktiven Maschinen zurück (Disabled=0, Deprovisioned=0) aus `AllActiveMachinesView`. +EN: Returns all active machines (Disabled=0, Deprovisioned=0) from `AllActiveMachinesView`. + +**Returns**: `Task>`. + +--- + +#### GetAllActiveMachinesWithNetworkInfoAsync + +```csharp +public async Task> GetAllActiveMachinesWithNetworkInfoAsync() +``` + +**Beschreibung / Description**: +DE: Gibt aktive Maschinen mit mindestens einem nicht-leeren Netzwerkwert zurück + (IPv4, IPv6 oder FQDN) aus `AllActiveMachinesWithNetworkInfoView`. +EN: Returns active machines with at least one non-empty network value + (IPv4, IPv6, or FQDN) from `AllActiveMachinesWithNetworkInfoView`. + +**Returns**: `Task>`. + +--- + +#### GetAllDisabledMachinesAsync + +```csharp +public async Task> GetAllDisabledMachinesAsync() +``` + +**Beschreibung / Description**: +DE: Gibt alle deaktivierten Maschinen zurück (Disabled=1, Deprovisioned=0) aus `AllDisabledMachinesView`. +EN: Returns all disabled machines (Disabled=1, Deprovisioned=0) from `AllDisabledMachinesView`. + +**Returns**: `Task>`. + +--- + +#### GetAllDeprovisionedMachinesAsync + +```csharp +public async Task> GetAllDeprovisionedMachinesAsync() +``` + +**Beschreibung / Description**: +DE: Gibt alle deprovisionierten Maschinen zurück (Disabled=1, Deprovisioned=1) aus + `AllDeprovisionedMachinesView`. +EN: Returns all deprovisioned machines (Disabled=1, Deprovisioned=1) from + `AllDeprovisionedMachinesView`. + +**Returns**: `Task>`. + +--- + +### Lesen / Read Methods — Einzelne Maschine / Single Machine + +#### GetMachineByIdAsync + +```csharp +public async Task GetMachineByIdAsync(int id) +``` + +**Beschreibung / Description**: +DE: Gibt die Maschine mit der angegebenen Id zurück, oder null wenn nicht gefunden. +EN: Returns the machine with the given Id, or null if not found. + +**Parameters**: `id`: `int` — Maschinen-Id / Machine Id. +**Returns**: `Task`. + +--- + +#### GetMachineByNameAsync + +```csharp +public async Task GetMachineByNameAsync(string machineName) +``` + +**Beschreibung / Description**: +DE: Gibt die Maschine mit dem angegebenen Namen zurück, oder null wenn nicht gefunden. +EN: Returns the machine with the given name, or null if not found. + +**Parameters**: `machineName`: `string` — Maschinenname / Machine name. +**Returns**: `Task`. + +--- + +### Lesen / Read Methods — Inventar-Snapshots / Inventory Snapshots + +#### GetLatestHardwareInventoryAsync + +```csharp +public async Task GetLatestHardwareInventoryAsync(int machineId) +``` + +**Beschreibung / Description**: +DE: Gibt den neuesten Hardware-Inventar-Eintrag für die angegebene Maschine zurück. +EN: Returns the most recent hardware inventory record for the specified machine. + +**Parameters**: `machineId`: `int`. +**Returns**: `Task` — null wenn keine Daten vorhanden / null if no data. + +--- + +#### GetLatestSoftwareInventoryAsync + +```csharp +public async Task GetLatestSoftwareInventoryAsync(int machineId) +``` + +**Beschreibung / Description**: +DE: Gibt den neuesten Software-Inventar-Eintrag für die angegebene Maschine zurück. +EN: Returns the most recent software inventory record for the specified machine. + +**Parameters**: `machineId`: `int`. +**Returns**: `Task` — null wenn keine Daten vorhanden / null if no data. + +--- + +### Wartung / Maintenance + +#### CleanupOldRecordsAsync + +```csharp +public async Task CleanupOldRecordsAsync(int daysToKeep = 30) +``` + +**Beschreibung / Description**: +DE: Löscht Hardware- und Software-Inventareinträge, die älter als `daysToKeep` Tage sind. + `daysToKeep=0` löscht alle Einträge (kein Minimum erzwungen). +EN: Deletes hardware and software inventory records older than `daysToKeep` days. + `daysToKeep=0` deletes all records (no minimum enforced). + +**Parameters**: `daysToKeep`: `int` — Aufbewahrungsdauer in Tagen / Retention period in days (default: 30). +**Returns**: `Task` (void async). +**Throws**: `NpgsqlException` bei Verbindungsfehler oder PostgreSQL-Fehler (propagiert ungefangen). + +--- + +### Existenz-Checks / Existence Checks + +#### HasMachineRecordsAsync + +```csharp +public async Task HasMachineRecordsAsync() +``` + +**Returns**: `Task` — `true` wenn mind. eine Maschine in `Machines` existiert. + +--- + +#### HasHardwareInventoryRecordsAsync + +```csharp +public async Task HasHardwareInventoryRecordsAsync() +``` + +**Returns**: `Task` — `true` wenn mind. ein Eintrag in `HardwareInventories` existiert. + +--- + +#### HasSoftwareInventoryRecordsAsync + +```csharp +public async Task HasSoftwareInventoryRecordsAsync() +``` + +**Returns**: `Task` — `true` wenn mind. ein Eintrag in `SoftwareInventories` existiert. + +--- + +### Zähler / Count Methods + +#### GetMachineCountAsync + +```csharp +public async Task GetMachineCountAsync() +``` + +**Returns**: `Task` — Gesamtanzahl Maschinen in `Machines`. + +--- + +#### GetHardwareInventoryCountAsync + +```csharp +public async Task GetHardwareInventoryCountAsync() +``` + +**Returns**: `Task` — Gesamtanzahl Einträge in `HardwareInventories`. + +--- + +#### GetSoftwareInventoryCountAsync + +```csharp +public async Task GetSoftwareInventoryCountAsync() +``` + +**Returns**: `Task` — Gesamtanzahl Einträge in `SoftwareInventories`. + +--- + +### Import + +#### InitializeMachinesFromCsvAsync + +```csharp +public async Task InitializeMachinesFromCsvAsync(string csvFilePath) +``` + +**Beschreibung / Description**: +DE: Importiert Maschinen aus einer CSV-Datei in `Machines`. Bestehende Maschinen werden übersprungen + (nur neue werden eingefügt). Die gesamte Operation läuft innerhalb einer Transaktion — bei + Exception: vollständiger Rollback. +EN: Imports machines from a CSV file into `Machines`. Existing machines are skipped + (only new entries are inserted). The entire operation runs within a transaction — + on exception: full rollback. + +**Implementierungsdetail / Implementation detail**: `NpgsqlConnection` und `NpgsqlTransaction` +werden mit `await using` (nicht `using`) verwaltet, da beide `IAsyncDisposable` implementieren. +Pattern: `await using var connection = new NpgsqlConnection(...); await using var transaction = await connection.BeginTransactionAsync();` +/ `NpgsqlConnection` and `NpgsqlTransaction` are managed with `await using` (not `using`) since +both implement `IAsyncDisposable`. This mirrors the SQLite implementation pattern (research.md §R-09). + +**Parameters**: `csvFilePath`: `string` — Pfad zur CSV-Datei / Path to CSV file. +**Returns**: `Task` — Anzahl erfolgreich importierter Maschinen / Count of successfully imported machines. +**Throws**: +- `FileNotFoundException` wenn CSV-Datei nicht gefunden / if CSV file not found. +- `Exception` (wrapped) bei Importfehler mit Rollback-Hinweis. +- `NpgsqlException` bei Verbindungsfehler (propagiert vor Rollback). + +--- + +## ServiceContainer-Änderungen / ServiceContainer Changes + +Die folgenden drei Änderungen sind in `Initialize.cs` (Klasse `ServiceContainer`) erforderlich: +The following three changes are required in `Initialize.cs` (class `ServiceContainer`): + +### (1) Property wird nullable / Property becomes nullable + +```csharp +// Vorher / Before: +public PgSqlDbService PgSqlDbService { get; } + +// Nachher / After: +public PgSqlDbService? PgSqlDbService { get; } +``` + +`Initialize.Services(Settings settings)` gibt `null` zurück wenn `WriteEnabled == false`. +`Initialize.Services()` (parameterlos / parameterless) gibt immer `null` zurück. + +### (2) Konstruktor: ArgumentNullException entfernen / Constructor: remove ArgumentNullException + +```csharp +// Vorher / Before: +PgSqlDbService = pgSqlDbService ?? throw new ArgumentNullException(nameof(pgSqlDbService)); + +// Nachher / After: +PgSqlDbService = pgSqlDbService; // null ist erlaubt / null is allowed +``` + +### (3) Dispose-Methoden: null-Check hinzufügen / Dispose methods: add null-check + +In `Dispose(bool disposing)` und `DisposeAsyncCore()` muss vor dem Dispose von `PgSqlDbService` +ein null-Check ergänzt werden (analog zur bestehenden Dispose-Logik für andere Services): +In `Dispose(bool disposing)` and `DisposeAsyncCore()`, add a null-check before disposing +`PgSqlDbService` (following the existing null-safe dispose pattern for other services): + +```csharp +// In Dispose(bool disposing) — neu hinzufügen / add new: +if (PgSqlDbService is IDisposable pgDisposable) +{ + pgDisposable.Dispose(); +} + +// In DisposeAsyncCore() — neu hinzufügen / add new: +if (PgSqlDbService is IAsyncDisposable pgAsyncDisposable) +{ + disposeTasks.Add(pgAsyncDisposable.DisposeAsync().AsTask()); +} +else if (PgSqlDbService is IDisposable pgDisposable2) +{ + disposeTasks.Add(Task.Run(() => pgDisposable2.Dispose())); +} +``` + +--- + +## Deferred: Statistik-View-Lesemethoden / Deferred: Statistics View Read Methods + +Die folgenden Statistik-Views werden in `PgSqlDbService.InitializeDatabase()` angelegt, erhalten +aber in **diesem Feature keine C#-Lesemethoden**. Sie sind explizit per spec.md §Assumptions aus +dem Scope ausgeklammert und werden in einem separaten Lastenheft implementiert (research.md §R-10): +/ The following statistics views are created in `PgSqlDbService.InitializeDatabase()` but receive +**no C# read methods in this feature**. They are explicitly excluded per spec.md §Assumptions and +will be implemented in a separate Lastenheft (research.md §R-10): + +- `ComputerModelStatisticsView` +- `ArchitectureStatisticsView` +- `ModelArchitectureStatisticsView` +- `HardwareStatisticsOverview` + +--- + +## Worker.cs-Aufrufe / Worker.cs Call Pattern + +SQLite ist die führende Datenbank. Die SQLite-Id wird explizit in `machine.Id` gesetzt, bevor +PgSQL aufgerufen wird. Dadurch übernimmt PostgreSQL dieselbe Id, und `_machineId` kann sicher für +alle FK-Schreibzugriffe (`SaveHardwareInventoryAsync`, `SaveSoftwareInventoryAsync`) genutzt werden. + +```csharp +// SQLite-Id zuerst holen und in machine.Id setzen: +// Get SQLite Id first and set it in machine.Id: +_machineId = await _sqliteDbService.SaveOrUpdateMachineAsync(machine, isHarvester: true); +machine.Id = _machineId; + +// Danach PgSQL mit derselben Id: +// Then PostgreSQL with the same Id: +if (_pgSqlDbService != null) +{ + try + { + await _pgSqlDbService.SaveOrUpdateMachineAsync(machine, isHarvester: true); + await _pgSqlDbService.SaveHardwareInventoryAsync(_machineId, hardwareInventory); + await _pgSqlDbService.SaveSoftwareInventoryAsync(_machineId, softwareInventory); + } + catch (Exception pgException) + { + HandleException(pgException); + // SQLite/MongoDB writes continue regardless + } +} +``` diff --git a/specs/001-pgsql-paritaet/data-model.md b/specs/001-pgsql-paritaet/data-model.md new file mode 100644 index 0000000..44dcf89 --- /dev/null +++ b/specs/001-pgsql-paritaet/data-model.md @@ -0,0 +1,205 @@ +# Data Model: PostgreSQL-Parität zum SqliteDbService + +**Branch**: `001-pgsql-paritaet` | **Date**: 2026-04-18 +**Voraussetzung / Requirement**: PostgreSQL 14 oder höher — benötigt für `GENERATED BY DEFAULT AS IDENTITY`, `RETURNING Id` und `timestamptz` (spec.md §Assumptions, plan.md §Constraints). + +--- + +## Entitäten / Entities + +### Machine (Tabelle: `Machines`) + +Stammdaten einer verwalteten Maschine. Primäre Lookup-Entität für alle Inventarabfragen. + +| Spalte / Column | Typ / Type | Constraint | Beschreibung / Description | +|------------------|--------------------|-------------------------------------------|-----------------------------------------------------| +| `Id` | `int` | `GENERATED BY DEFAULT AS IDENTITY PK` | Surrogat-Schlüssel / Surrogate key | +| `Name` | `TEXT NOT NULL` | `UNIQUE` | Eindeutiger Maschinenname / Unique machine name | +| `OperatingSystem`| `TEXT` | nullable | Betriebssystem / Operating system string | +| `LastSeen` | `timestamptz` | nullable | Letzter API-Kontakt / Last API contact | +| `CreatedAt` | `timestamptz` | `DEFAULT now()` | Zeitstempel der Erstellung / Record creation time | +| `IPv4` | `TEXT` | nullable | IPv4-Adresse / IPv4 address | +| `IPv6` | `TEXT` | nullable | IPv6-Adresse / IPv6 address | +| `FQDN` | `TEXT` | nullable | Fully Qualified Domain Name | +| `Disabled` | `BOOLEAN NOT NULL` | `DEFAULT FALSE` | Deaktiviert-Flag / Disabled flag; C# `bool`; Npgsql-Direktmapping | +| `Deprovisioned` | `BOOLEAN NOT NULL` | `DEFAULT FALSE` | Deprovisioniert-Flag / Deprovisioned flag; C# `bool` | +| `LastHarvested` | `timestamptz` | nullable | Letzter Ernte-Zeitpunkt / Last harvested timestamp | + +**C# Model**: `InventarWorkerCommon.Models.SqlDatabase.Machine` (bestehend / existing) +**Index**: `idx_machines_name ON Machines(Name)` + +--- + +### HardwareInventories (Tabelle: `HardwareInventories`) + +Hardware-Momentaufnahme einer Maschine. Jeder Ernte-Zyklus fügt einen neuen Datensatz ein. + +| Spalte / Column | Typ / Type | Constraint | Beschreibung / Description | +|------------------------|---------------|--------------------------------------------|---------------------------------------------------| +| `Id` | `int` | `GENERATED BY DEFAULT AS IDENTITY PK` | Surrogat-Schlüssel / Surrogate key | +| `MachineId` | `int` | `FK → Machines(Id)` | Referenz auf Maschine / Reference to machine | +| `ComputerName` | `TEXT` | nullable | Rechnername / Computer hostname | +| `ComputerModel` | `TEXT` | nullable | Modellbezeichnung / Hardware model name | +| `ComputerManufacturer` | `TEXT` | nullable | Hersteller / Manufacturer | +| `Architecture` | `TEXT` | nullable | CPU-Architektur / CPU architecture (x64, ARM64) | +| `ProcessorName` | `TEXT` | nullable | CPU-Bezeichnung / Processor name | +| `ProcessorCores` | `INTEGER` | nullable | Anzahl Kerne / Core count | +| `TotalMemoryGB` | `REAL` | nullable | Gesamtspeicher in Bytes / Total memory bytes | +| `AvailableMemoryGB` | `REAL` | nullable | Verfügbarer Speicher / Available memory bytes | +| `MemoryUsagePercent` | `REAL` | nullable | Speicherauslastung % / Memory usage percentage | +| `CreatedAt` | `timestamptz` | `DEFAULT now()` | Zeitstempel / Record timestamp | + +**C# Model**: `InventarWorkerCommon.Models.SqlDatabase.HardwareInventories` (bestehend / existing) +**Index**: `idx_hardware_machine_created ON HardwareInventories(MachineId, CreatedAt)` +**FK-Verhalten / FK Behavior**: `MachineId → Machines(Id)` — kein explizites `ON DELETE`-Constraint. PostgreSQL-Standard: `RESTRICT`. Eine Maschine kann nicht gelöscht werden, solange Hardware-Inventareinträge auf sie referenzieren. Dieses Verhalten ist bewusst gewählt (sicheres Default, kein versehentlicher Datenverlust). / No explicit `ON DELETE` constraint; PostgreSQL default is `RESTRICT`. A machine cannot be deleted while hardware inventory records reference it. This is the intentional safe default. + +--- + +### SoftwareInventories (Tabelle: `SoftwareInventories`) + +Software-Momentaufnahme als JSON-serialisierte Teillisten. Ein Datensatz pro Ernte-Zyklus. + +| Spalte / Column | Typ / Type | Constraint | Beschreibung / Description | +|-------------------------|---------------|-------------------------------------------|-------------------------------------------------------| +| `Id` | `int` | `GENERATED BY DEFAULT AS IDENTITY PK` | Surrogat-Schlüssel / Surrogate key | +| `MachineId` | `int` | `FK → Machines(Id)` | Referenz auf Maschine / Reference to machine | +| `ProcessesJson` | `TEXT` | nullable | Laufende Prozesse (JSON) / Running processes (JSON) | +| `InstalledSoftwareJson` | `TEXT` | nullable | Installierte Software (JSON) / Installed software | +| `ServicesJson` | `TEXT` | nullable | Windows-Dienste (JSON) / Windows services (JSON) | +| `EnvironmentJson` | `TEXT` | nullable | Umgebungsvariablen (JSON) / Environment variables | +| `StartupProgramsJson` | `TEXT` | nullable | Autostart-Programme (JSON) / Startup programs (JSON) | +| `RuntimeJson` | `TEXT` | nullable | Runtime-Info (JSON) / Runtime information (JSON) | +| `CreatedAt` | `timestamptz` | `DEFAULT now()` | Zeitstempel / Record timestamp | + +**C# Model**: `InventarWorkerCommon.Models.SqlDatabase.SoftwareInventories` (bestehend / existing) +**JSON-Serialisierung**: `System.Text.Json` mit camelCase-Policy (constitution Principle V) +**Index**: `idx_software_machine_created ON SoftwareInventories(MachineId, CreatedAt)` +**FK-Verhalten / FK Behavior**: `MachineId → Machines(Id)` — kein explizites `ON DELETE`-Constraint. PostgreSQL-Standard: `RESTRICT` (identisch zu HardwareInventories). / No explicit `ON DELETE` constraint; PostgreSQL default is `RESTRICT` (same as HardwareInventories). + +--- + +### MachineState (View-Projektion, kein eigenes Modell) + +Projektion für alle View-basierten Lesemethoden (`AllActiveMachinesView` etc.). +Kein eigenes Datenbankschema — wird von Dapper aus View-Ergebnissen gemappt. + +| Feld / Field | Typ / Type | Quelle / Source | +|-----------------|-------------|----------------------------| +| `Id` | `int` | `Machines.Id` | +| `Name` | `string` | `Machines.Name` | +| `IPv4` | `string?` | `Machines.IPv4` | +| `IPv6` | `string?` | `Machines.IPv6` | +| `FQDN` | `string?` | `Machines.FQDN` | +| `Disabled` | `bool` | `Machines.Disabled` | +| `Deprovisioned` | `bool` | `Machines.Deprovisioned` | +| `LastSeen` | `DateTime?` | `Machines.LastSeen` | +| `LastHarvested` | `DateTime?` | `Machines.LastHarvested` | + +**C# Model**: `InventarWorkerCommon.Models.SqlDatabase.MachineState` (bestehend / existing) + +--- + +### MachineFromCsv (CSV-Import-Hilfsmodell) + +Wird ausschließlich von `InitializeMachinesFromCsvAsync` gelesen. Kein Datenbankschema. +C# Model: `InventarWorkerCommon.Models.SqlDatabase.MachineFromCsv` (bestehend / existing) + +--- + +## Views (PostgreSQL) + +### HardwareInventoryView *(umbenannt von `hardware_inventory_view`)* + +Aggregiert Hardware-Informationen pro Maschine (letzter Eintrag pro Maschine). +`DROP VIEW IF EXISTS hardware_inventory_view` wird in `InitializeDatabase()` ausgeführt (FR-013). + +```sql +CREATE OR REPLACE VIEW HardwareInventoryView AS +SELECT DISTINCT ON (m.id) + m.id AS MachineID, + m.name AS MachineName, + h.Architecture, + h.ProcessorCores, + ROUND(h.TotalMemoryGB::numeric / 1024 / 1024 / 1024, 2) AS TotalMemoryGB, + ROUND(h.AvailableMemoryGB::numeric / 1024 / 1024 / 1024, 2) AS AvailableMemoryGB, + ROUND(h.MemoryUsagePercent::numeric, 2) AS MemoryUsagePercent +FROM Machines m + JOIN HardwareInventories h ON h.MachineId = m.Id +ORDER BY m.Id, h.CreatedAt DESC; +``` + +**Spalten-Aliase**: PascalCase (Parität zu SQLite-View). Identische Spaltenbezeichnungen wie SQLite. + +**Semantische Änderung / Semantic change**: Die alte `hardware_inventory_view` verwendete +`ORDER BY m.id, m.name ASC` als zweiten Sortierschlüssel. Da `m.name` für alle Zeilen derselben +Maschine identisch ist, lieferte PostgreSQL für `DISTINCT ON (m.id)` einen nicht-deterministischen +Eintrag (nicht zwingend den neuesten). Die neue `HardwareInventoryView` verwendet +`ORDER BY m.id, h.CreatedAt DESC` und wählt damit immer den zeitlich neuesten Hardware-Inventar- +eintrag je Maschine. Dies ist eine semantische Korrektur, nicht nur eine Umbenennung. Zusätzlich +werden die Spalten-Aliase von snake_case auf PascalCase vereinheitlicht. +/ The old `hardware_inventory_view` used `ORDER BY m.id, m.name ASC` as the second sort key. +Since `m.name` is identical for all rows of the same machine, PostgreSQL returned a non-deterministic +row for `DISTINCT ON (m.id)` — not necessarily the most recent one. The new `HardwareInventoryView` +uses `ORDER BY m.id, h.CreatedAt DESC` and always selects the latest hardware inventory entry per +machine. This is a semantic fix, not merely a rename. Column aliases are also unified from snake_case +to PascalCase. + +### Weitere bestehende Views (keine Änderung außer Erstellung) + +| View | Beschreibung / Description | +|---------------------------------------|-----------------------------------------------------------------| +| `LatestSoftwareInventoriesView` | Neuestes Software-Inventar pro Maschine | +| `LatestHardwareInventoriesView` | Neuestes Hardware-Inventar pro Maschine (per ROW_NUMBER) | +| `ComputerModelStatisticsView` | Statistik nach Computermodell (in InitializeDatabase vorhanden) | +| `ArchitectureStatisticsView` | Statistik nach Architektur (in InitializeDatabase vorhanden) | +| `ModelArchitectureStatisticsView` | Kombinierte Modell/Architektur-Statistik | +| `HardwareStatisticsOverview` | Überblick Hardware-Statistiken | +| `AllActiveMachinesView` | Aktive Maschinen (Disabled=FALSE, Deprovisioned=FALSE) | +| `AllActiveMachinesWithNetworkInfoView`| Aktive Maschinen mit mind. einem Netzwerkwert | +| `AllDisabledMachinesView` | Deaktivierte Maschinen (Disabled=TRUE, Deprovisioned=FALSE) | +| `AllDeprovisionedMachinesView` | Deprovisionierte Maschinen (Disabled=TRUE, Deprovisioned=TRUE) | + +**C#-Methoden-Abdeckung in diesem Feature / C# method coverage in this feature**: + +Views MIT C#-Read-Methode in diesem Feature / Views WITH a C# Read-Method in this feature: +- `AllActiveMachinesView` → `GetAllActiveMachinesAsync()` +- `AllActiveMachinesWithNetworkInfoView` → `GetAllActiveMachinesWithNetworkInfoAsync()` +- `AllDisabledMachinesView` → `GetAllDisabledMachinesAsync()` +- `AllDeprovisionedMachinesView` → `GetAllDeprovisionedMachinesAsync()` + +Views OHNE C#-Read-Methode in diesem Feature / Views WITHOUT a C# Read-Method in this feature: +- `HardwareInventoryView` — Schema wird angelegt; keine eigene Lesemethode in diesem Feature; dient zur manuellen SQL-Verifikation +- `LatestSoftwareInventoriesView`, `LatestHardwareInventoriesView` — interne Hilfs-Views für die Statistik-Views; keine eigene C#-Methode geplant +- `ComputerModelStatisticsView`, `ArchitectureStatisticsView`, `ModelArchitectureStatisticsView`, `HardwareStatisticsOverview` — Statistik-Views; C#-Lesemethoden explizit deferred (spec.md §Assumptions, research.md §R-10, contracts/PgSqlDbService-methods.md) + +--- + +## Namenskonventionen / Naming Conventions + +| Kontext / Context | Konvention / Convention | Beispiel / Example | +|---------------------------|-------------------------|-----------------------------------------| +| Tabellen / Tables | PascalCase | `Machines`, `HardwareInventories` | +| Spalten / Columns | PascalCase | `MachineId`, `CreatedAt`, `IPv4` | +| Views | PascalCase + `View` | `HardwareInventoryView`, `AllActiveMachinesView` | +| Indizes / Indexes | snake_case + `idx_` | `idx_machines_name` | +| FK-Spalten / FK columns | `{Entity}Id` | `MachineId` | + +--- + +## Typ-Hinweise / Type Notes + +**Disabled / Deprovisioned**: PostgreSQL `BOOLEAN`, C# `bool` (in `Machine`, `MachineState`, `MachineFromCsv` — alle drei bereits `bool`). Npgsql mappt `BOOLEAN` ↔ `bool` nativ ohne Typkonvertierung. CsvHelper konvertiert `"0"` → `false` und `"1"` → `true` standardmäßig per `BooleanConverter`. SQLite verwendet weiterhin `INTEGER NOT NULL DEFAULT 0` (keine Änderung dort nötig — Dapper mappt SQLite-INTEGER zu `bool` implizit). + +View-WHERE-Klauseln müssen bei `BOOLEAN`-Spalten `= FALSE` / `= TRUE` verwenden, nicht `= 0` / `= 1` (PostgreSQL erlaubt keine Integer-Vergleiche mit BOOLEAN). + +--- + +## Änderungen gegenüber bestehendem Code / Changes vs. Existing Code + +| Datei / File | Änderung / Change | +|------------------------------------------|--------------------------------------------------------------------------------------------| +| `PgSqlDbService.cs` | `Disabled`/`Deprovisioned`: `INTEGER DEFAULT 0` → `BOOLEAN DEFAULT FALSE`; alle View-WHERE-Klauseln `= 0`/`= 1` → `= FALSE`/`= TRUE`; 20 neue Methoden; `InitializeDatabase()` um DROP VIEW erweitern | +| `Initialize.cs` (Services-Overloads) | WriteEnabled-Check → null return; Fallback-Overload überspringt PgSQL | +| `ServiceContainer` (in Initialize.cs) | `PgSqlDbService` wird zu `PgSqlDbService?` (nullable) | +| `Worker.cs` | PgSQL-Schreibaufrufe hinzufügen; null-Check vor jedem PgSQL-Call | +| `PgSqlDbServiceTest.cs` (NEU) | Unit-Tests + Integrationstests für alle 21 Methoden | diff --git a/specs/001-pgsql-paritaet/plan.md b/specs/001-pgsql-paritaet/plan.md new file mode 100644 index 0000000..ff3fd8d --- /dev/null +++ b/specs/001-pgsql-paritaet/plan.md @@ -0,0 +1,223 @@ +# Implementation Plan: PostgreSQL-Parität zum SqliteDbService + +**Branch**: `001-pgsql-paritaet` | **Date**: 2026-04-18 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/001-pgsql-paritaet/spec.md` + +--- + +## Summary + +`PgSqlDbService` erhält alle 21 öffentlichen Methoden des `SqliteDbService`, sodass PostgreSQL als +vollwertiger paralleler Persistenz-Provider neben SQLite und MongoDB genutzt werden kann. Der +`HarvesterWorkerService` schreibt Inventardaten bei aktiviertem `WriteEnabled` simultan in alle drei +Datenbanken. PostgreSQL-Fehler sind isoliert — SQLite/MongoDB-Writes laufen weiter. Die View +`hardware_inventory_view` wird auf `HardwareInventoryView` (PascalCase) umbenannt. + +Technisch: Dapper + Npgsql, `RETURNING Id` statt `last_insert_rowid()`, `DateTime.UtcNow` für +alle `timestamptz`-Spalten, null-Pattern für `PgSqlDbService?` im `ServiceContainer`. + +--- + +## Technical Context + +**Language/Version**: C# 14.0 on .NET 10 +**Primary Dependencies**: Npgsql 10.0.1 (bestehend / existing), Dapper 2.1.72 (bestehend), + CsvHelper 33.1.0 (bestehend), System.Text.Json (BCL, kein separates Paket) +**Storage**: PostgreSQL 14+ (Ziel / target), SQLite (bestehend), MongoDB (bestehend) +**Testing**: MSTest 4.1.0 + coverage gates (>=70% minimum, target >=80%); + Unit-Tests ohne echte DB; Integrationstests (`[TestCategory("Integration")]`) mit echter PgSQL-Instanz +**Target Platform**: Cross-platform (Windows Service / systemd / launchd) +**Project Type**: Shared library (`InventarWorkerCommon`) + Background service (`HarvesterWorkerService`) +**Performance Goals**: Schreib-Durchsatz vergleichbar mit SQLite (async I/O); Lese-Latenz < 500 ms für + Listen-Abfragen bei ~200 Maschinen. + *Hinweis / Note*: Der Schreib-Durchsatz ist bewusst nicht absolut quantifiziert. Bei Single-Writer, + ~200 Maschinen und async I/O mit Dapper ist kein absoluter Schwellenwert notwendig. Eine gespeicherte + SQLite-Basismessung existiert nicht — "vergleichbar mit SQLite" bedeutet: async Dapper-Schreibzugriff + auf lokaler PostgreSQL-Instanz unter gleicher Last. [CHK004/CHK005: Akzeptierte Unschärfe — MVP-Scope] +**Constraints**: Kein `IDbService`-Interface in diesem Feature (deferred); `InitializeDatabase()` + bleibt synchron (Parität zu SQLite); PostgreSQL 14+ vorausgesetzt +**Scale/Scope**: ~200 Maschinen, ~10K Inventar-Datensätze pro Tag, Single-Writer-Modell + +--- + +## Constitution Check + +*GATE: Alle Gates bestehen vor Phase 0. Re-Check nach Phase 1 unten.* + +- **Branching Gate**: ✅ Branch `001-pgsql-paritaet` existiert und ist nicht `main`. + Merge-Pfad: PR zu `main`. Keine Feature-Commits direkt auf `main`. + +- **Toolchain Gate**: ✅ `InventarWorkerCommon` und `HarvesterWorkerService` sind bereits auf + .NET 10 / C# 14.0 (`LangVersion 14.0`). Kein Upgrade erforderlich. + +- **Architecture Gate**: ✅ Alle Kernänderungen in `InventarWorkerCommon/Services/Database/PgSqlDbService.cs` + und `InventarWorkerCommon/Services/Common/Initialize.cs`. Worker-Anpassungen in + `HarvesterWorkerService/Worker.cs`. Keine Verletzung der Layer-Grenzen. + +- **Documentation Gate**: ✅ FR-017 mandatiert vollständige XML-Dokumentation für alle 21 neuen + öffentlichen Methoden. Zweisprachig DE/EN, CEFR B2 (constitution Principle I + III). + +- **XML/DocFX Gate**: ✅ `docfx docfx.json` wird als letzter Schritt der Polish-Phase ausgeführt, + nachdem alle API-Signaturen und XML-Kommentare finalisiert sind (CA-007). + +- **Testing/Coverage Gate**: ✅ Red-Green-Refactor für alle 21 neuen Methoden definiert. + Coverage-Plan: Unit-Tests (Logik, null-Checks) + Integrationstests (full method coverage). + CI-Gate ≥70%; Ziel ≥80%. + +- **Dependency Currency Gate**: ✅ Alle betroffenen Pakete sind auf aktueller stabiler Version: + Npgsql 10.0.1, Dapper 2.1.72, CsvHelper 33.1.0, MSTest 4.1.0. Kein Pinning erforderlich. + `dotnet list package --outdated` wird als Polish-Task ausgeführt. + +- **Data Contract Gate**: ✅ `System.Text.Json` mit camelCase für JSON-Serialisierung (FR-003). + Dapper mit expliziten SQL-Strings. PascalCase für Tabellen/Spalten. View-Umbenennung per + `DROP VIEW IF EXISTS hardware_inventory_view` + `CREATE OR REPLACE VIEW HardwareInventoryView` (FR-013). + +- **Statistical Documentation Gate**: ✅ `docs/project-statistics.md` wird als Teil der Polish-Phase + mit Branch-Scope, Code/Test/Doc-Zeilenzahlen und manuellem Baseline-Eintrag aktualisiert. + +**Post-Phase-1 Re-Check**: Alle Gates bestehen nach dem Design. Kein Eintrag in Complexity Tracking +erforderlich. + +--- + +## Project Structure + +### Documentation (this feature) + +```text +specs/001-pgsql-paritaet/ +├── plan.md # Dieses Dokument / This document +├── spec.md # Feature-Spezifikation / Feature specification +├── research.md # Phase 0 — Alle Entscheidungen dokumentiert / All decisions documented +├── data-model.md # Phase 1 — Datenbankschema und Entitäten / DB schema and entities +├── quickstart.md # Phase 1 — Validierungsanleitung / Validation guide +├── contracts/ +│ └── PgSqlDbService-methods.md # Alle 21 Methodensignaturen / All 21 method signatures +├── checklists/ +│ └── requirements.md # Vollständig abgehakt / Fully checked +└── tasks.md # Phase 2 — Wird von /speckit.tasks erstellt / Created by /speckit.tasks +``` + +### Source Code + +```text +InventarWorkerCommon/ +├── Services/ +│ ├── Database/ +│ │ └── PgSqlDbService.cs # PRIMÄR: 20 neue Methoden + InitializeDatabase()-Erweiterung +│ └── Common/ +│ └── Initialize.cs # WriteEnabled-Guard + Fallback-Pfad; ServiceContainer.PgSqlDbService? +└── Models/ + └── SqlDatabase/ # Keine neuen Modelle — alle bestehend (Machine, HardwareInventories, + ├── Machine.cs # SoftwareInventories, MachineState, MachineFromCsv) + ├── HardwareInventories.cs + ├── SoftwareInventories.cs + ├── MachineState.cs + └── MachineFromCsv.cs + +HarvesterWorkerService/ +└── Worker.cs # PgSQL-Schreibaufrufe + null-Check vor jedem PgSQL-Call + +InventarWorkerCommonTest/ +└── PgSqlDbServiceTest.cs # NEU: Unit-Tests (Logik) + Integrationstests (full coverage) +``` + +**Structure Decision**: Multi-project, bestehende Struktur (constitution Principle II). Kein neues +Projekt erforderlich. Alle Shared-Logic-Änderungen in `InventarWorkerCommon`, Runtime-spezifische +Änderungen in `HarvesterWorkerService`. + +--- + +## Complexity Tracking + +> Keine Constitution-Verletzungen — kein Eintrag erforderlich. + +--- + +## Implementation Notes + +### PgSqlDbService — Kritische Implementierungsdetails + +1. **RETURNING Id**: Alle INSERT-Statements in `PgSqlDbService` müssen `RETURNING Id` verwenden. + Dapper: `await connection.QuerySingleAsync(insertQuery, parameters)`. + +2. **DateTime.UtcNow**: Alle DateTime-Werte an PostgreSQL via `CreatedAt = DateTime.UtcNow`, + `LastSeen = DateTime.UtcNow` etc. Npgsql 6+ lehnt `DateTimeKind.Local` für `timestamptz` ab. + +3. **DROP VIEW vor HardwareInventoryView**: In `InitializeDatabase()` muss VOR dem ersten + `CREATE OR REPLACE VIEW HardwareInventoryView` folgendes stehen: + ```sql + DROP VIEW IF EXISTS hardware_inventory_view; + ``` + +4. **Null-Pattern für ServiceContainer**: `ServiceContainer.PgSqlDbService` wird zu `PgSqlDbService?`. + `Initialize.Services(Settings settings)`: gibt `null` zurück wenn `WriteEnabled == false`. + `Initialize.Services()` (Fallback): gibt immer `null` zurück. + Konstruktor: `ArgumentNullException` für `PgSqlDbService` entfernen (null ist erlaubt). + Dispose/DisposeAsync: null-Check vor `PgSqlDbService?.Dispose()` / `PgSqlDbService?.DisposeAsync()` hinzufügen + (analog zur bestehenden null-sicheren Disposal anderer Services im ServiceContainer). + +5. **CSV-Import**: `NpgsqlConnection` + `BeginTransaction()` analog zur SQLite-Implementierung. + `await using` für Connection und Transaction (IAsyncDisposable). + +### Worker.cs — Isoliertes Schreib-Pattern + +SQLite ist die **führende Datenbank** (authority). PostgreSQL ist eine optionale parallele Senke. +Die SQLite-Machine-Id wird explizit in `machine.Id` gesetzt, bevor `PgSqlDbService.SaveOrUpdateMachineAsync` +aufgerufen wird — so übernimmt PostgreSQL die SQLite-Id und alle FK-Referenzen in `HardwareInventories` +und `SoftwareInventories` bleiben konsistent. `GENERATED BY DEFAULT AS IDENTITY` erlaubt explizite +Id-Werte; das "BY DEFAULT" ermöglicht genau dieses Muster. + +```csharp +// SQLite ist führend — Id aus SQLite wird gesetzt und für PgSQL übernommen: +// SQLite is authoritative — Id from SQLite is set and adopted by PostgreSQL: +_machineId = await _sqliteDbService.SaveOrUpdateMachineAsync(machine, isHarvester: true); +machine.Id = _machineId; // SQLite-Id explizit setzen / Set SQLite Id explicitly + +if (_pgSqlDbService != null) +{ + try + { + // PgSqlDbService.SaveOrUpdateMachineAsync nutzt machine.Id für den INSERT: + // PgSqlDbService.SaveOrUpdateMachineAsync uses machine.Id for the INSERT: + await _pgSqlDbService.SaveOrUpdateMachineAsync(machine, isHarvester: true); + await _pgSqlDbService.SaveHardwareInventoryAsync(_machineId, hardwareInventory); + await _pgSqlDbService.SaveSoftwareInventoryAsync(_machineId, softwareInventory); + } + catch (Exception pgException) + { + HandleException(pgException); // Loggt, setzt Status = Error + // SQLite/MongoDB-Writes laufen weiter — kein rethrow + } +} +``` + +### Test-Strategie + +**Unit-Tests** (`InventarWorkerCommonTest/PgSqlDbServiceTest.cs`): +- Null-Check: `Initialize.Services()` gibt `null` für PgSqlDbService zurück +- Null-Check: `Initialize.Services(settings mit WriteEnabled=false)` gibt `null` zurück +- Exception-Verhalten: `SaveOrUpdateMachineAsync(null)` wirft ArgumentNullException +- Exception-Verhalten: `InitializeMachinesFromCsvAsync("nonexistent.csv")` wirft FileNotFoundException +- Logik: `CleanupOldRecordsAsync(0)` generiert korrekten Cutoff-Timestamp (DateTime.UtcNow) + +**Integrationstests** (`[TestCategory("Integration")]`): +- Alle 21 Methoden gegen echte PostgreSQL-Instanz +- Transaktion-Rollback-Test für CSV-Import +- View-Umbenennung: `hardware_inventory_view` nicht mehr vorhanden, `HardwareInventoryView` vorhanden +- `daysToKeep=0` löscht alle Einträge + +Verbindungsstring für Integrationstests via Umgebungsvariable: +`PGSQL_TEST_CONNECTION_STRING` = `Host=localhost;Port=5432;Database=inventar_test;Username=...` + +**CI-Filter-Befehle / CI filter commands**: +- Normaler CI-Lauf (ohne Integration): `dotnet test --filter "TestCategory!=Integration"` +- Nur Integrationstests: `dotnet test --filter "TestCategory=Integration"` + +**Test-Datenbank / Test database**: Dedizierte Datenbank `inventar_test` (nicht `inventar`). +Teardown: `ClassCleanup`-Methode führt `TRUNCATE TABLE SoftwareInventories, HardwareInventories, Machines RESTART IDENTITY CASCADE` (oder `DROP TABLE ... CASCADE`) durch. Keine Produktionsdaten werden berührt. + +**Coverage-Hinweis / Coverage note**: 5 Unit-Tests allein erreichen ≥70% auf ~800 Zeilen neuem +DB-Code nicht (DB-Methoden haben 0% ohne Integrationstests). Coverage-Gate ≥70% gilt für den +kombinierten Lauf (Unit + Integration). In reinen Unit-Test-CI-Umgebungen ohne PostgreSQL ist +Coverage < 70% akzeptiert und dokumentiert; das Gate gilt für den vollen Testlauf mit Integration. diff --git a/specs/001-pgsql-paritaet/quickstart.md b/specs/001-pgsql-paritaet/quickstart.md new file mode 100644 index 0000000..1e01aac --- /dev/null +++ b/specs/001-pgsql-paritaet/quickstart.md @@ -0,0 +1,277 @@ +# Quickstart: PostgreSQL-Parität zum SqliteDbService + +**Branch**: `001-pgsql-paritaet` | **Date**: 2026-04-18 + +Diese Anleitung beschreibt, wie die Implementierung nach Fertigstellung validiert werden kann. + +--- + +## Voraussetzungen / Prerequisites + +### 1. PostgreSQL-Instanz + +```bash +# macOS — PostgreSQL via Homebrew +brew install postgresql@16 +brew services start postgresql@16 + +# Oder Docker / Or Docker: +docker run --name inventar-pgsql \ + -e POSTGRES_PASSWORD=test \ + -e POSTGRES_USER=inventar \ + -e POSTGRES_DB=inventar \ + -p 5432:5432 \ + -d postgres:16 +``` + +### 2. .NET 10 SDK + +```bash +dotnet --version +# Erwartet / Expected: 10.x.x +``` + +### 3. Solution bauen / Build solution + +```bash +cd /path/to/InventarWorkerService +dotnet restore InventarWorkerService.sln +dotnet build InventarWorkerService.sln +# Erwartetes Ergebnis / Expected: Build succeeded, 0 Warning(s) +``` + +--- + +## Schritt 1: Datenbank initialisieren / Initialize Database + +DE: Stelle sicher, dass `InitializeDatabase()` ohne Fehler durchläuft. +EN: Verify that `InitializeDatabase()` runs without errors. + +```bash +# Umgebungsvariable setzen / Set environment variable: +export PGSQL_TEST_CONNECTION_STRING="Host=localhost;Port=5432;Database=inventar_test;Username=inventar;Password=test;" + +# Integrationstests ausführen / Run integration tests: +dotnet test InventarWorkerCommonTest/InventarWorkerCommonTest.csproj \ + --filter "TestCategory=Integration" + +# Normaler CI-Lauf ohne Integrationstests / Normal CI run without integration tests: +dotnet test InventarWorkerCommonTest/InventarWorkerCommonTest.csproj \ + --filter "TestCategory!=Integration" +``` + +> **Hinweis / Note**: Integrationstests verwenden eine dedizierte Test-Datenbank `inventar_test` +> (nicht `inventar`), die per `ClassCleanup` zwischen Testläufen bereinigt wird. + +Manuell verifizieren / Verify manually: + +```sql +-- Nach InitializeDatabase(): / After InitializeDatabase(): +\dt -- Zeigt / Shows: Machines, HardwareInventories, SoftwareInventories +\dv -- Zeigt / Shows: HardwareInventoryView, AllActiveMachinesView etc. + -- NICHT mehr / NOT anymore: hardware_inventory_view +``` + +--- + +## Schritt 2: User Story 1 validieren — Schreib-Methoden / Validate Write Methods + +```sql +-- PostgreSQL-Shell vorbereiten / Prepare PostgreSQL shell: +psql -h localhost -U inventar -d inventar +``` + +```csharp +// C# Smoke Test (in einem Testprojekt / in a test project): +var svc = new PgSqlDbService("Host=localhost;Port=5432;Database=inventar;Username=inventar;Password=test;"); +svc.InitializeDatabase(); + +var machine = new Machine { Name = "TEST-PC-01", OperatingSystem = "Windows 11" }; +var machineId = await svc.SaveOrUpdateMachineAsync(machine, isHarvester: true); +// Erwartet / Expected: machineId > 0 + +var hardware = new HardwareInventory { /* ... */ }; +await svc.SaveHardwareInventoryAsync(machineId, hardware); + +var software = new SoftwareInventory { /* ... */ }; +await svc.SaveSoftwareInventoryAsync(machineId, software); +``` + +```sql +-- Verifizieren / Verify: +SELECT * FROM Machines; -- 1 Zeile / row +SELECT COUNT(*) FROM HardwareInventories; -- 1 +SELECT COUNT(*) FROM SoftwareInventories; -- 1 +``` + +--- + +## Schritt 3: User Story 2 validieren — HarvesterWorkerService / Validate Worker Integration + +DE: Settings-Datei mit `WriteEnabled = true` vorbereiten. +EN: Prepare settings file with `WriteEnabled = true`. + +Typischer Speicherort / Typical settings file location: +- Windows: `%ProgramData%\InventarWorkerService\settings.ini` +- macOS/Linux: `/var/lib/inventar/settings.ini` + +```ini +[PgSqlDb] +PgSqlDbFqdn = localhost +PgSqlDbPort = 5432 +PgSqlDbName = inventar +PgSqlUser = inventar +PgSqlPassword = test +WriteEnabled = true +``` + +```bash +# HarvesterWorkerService starten / Start HarvesterWorkerService: +dotnet run --project HarvesterWorkerService/HarvesterWorkerService.csproj +# Im Debug-Modus läuft der Loop alle 30 Sekunden. +# In debug mode the loop runs every 30 seconds. +``` + +Nach einem Ernte-Zyklus prüfen / After one harvest cycle, verify: + +```sql +SELECT COUNT(*) FROM Machines; +SELECT COUNT(*) FROM HardwareInventories; +SELECT COUNT(*) FROM SoftwareInventories; +-- Alle drei Tabellen sollten Datensätze enthalten. +-- All three tables should contain records. +``` + +--- + +## Schritt 4: User Story 3 validieren — Lese-Methoden / Validate Read Methods + +```csharp +var machines = await svc.GetMachinesAsync(); +// Expected: Alle Maschinen / All machines + +var activeMachines = await svc.GetAllActiveMachinesAsync(); +// Expected: Nur aktive (Disabled=0, Deprovisioned=0) + +var count = await svc.GetMachineCountAsync(); +// Expected: Übereinstimmung mit GetMachinesAsync().Count + +var hasRecords = await svc.HasMachineRecordsAsync(); +// Expected: true + +var latest = await svc.GetLatestHardwareInventoryAsync(machineId); +// Expected: nicht null, CreatedAt ist der neueste Timestamp +``` + +--- + +## Schritt 5: User Story 4 validieren — CSV-Import / Validate CSV Import + +Test-CSV erstellen / Create test CSV: + +```csv +Name,OperatingSystem,IPv4,IPv6,FQDN,Disabled,Deprovisioned +CSV-MACHINE-01,Windows 10,192.168.1.10,,csv-01.local,0,0 +CSV-MACHINE-02,Windows 11,192.168.1.11,,csv-02.local,0,0 +CSV-MACHINE-03,Linux,192.168.1.12,,csv-03.local,0,0 +``` + +> **Hinweis / Note**: `MachineFromCsv.Disabled` und `.Deprovisioned` sind C# `bool`. +> CsvHelper konvertiert `"0"` → `false` und `"1"` → `true` automatisch per `BooleanConverter`. +> Die PostgreSQL-Spalten sind `BOOLEAN NOT NULL DEFAULT FALSE` — +> Npgsql mappt `bool` ↔ `BOOLEAN` direkt, keine manuelle Konvertierung nötig. + +```csharp +var importedCount = await svc.InitializeMachinesFromCsvAsync("/path/to/test-machines.csv"); +// Expected: importedCount == 3 + +// Erneuter Import / Re-import same file: +var importedCount2 = await svc.InitializeMachinesFromCsvAsync("/path/to/test-machines.csv"); +// Expected: importedCount2 == 0 (Duplikate werden übersprungen / duplicates skipped) +``` + +--- + +## Schritt 6: User Story 5 validieren — PascalCase Views / Validate PascalCase Views + +```sql +-- View-Name prüfen / Check view name: +SELECT * FROM HardwareInventoryView LIMIT 1; +-- Expected: Funktioniert / Works + +SELECT * FROM hardware_inventory_view LIMIT 1; +-- Expected: ERROR: relation "hardware_inventory_view" does not exist + +-- Spaltenaliase prüfen / Check column aliases: +SELECT column_name FROM information_schema.columns +WHERE table_name = 'hardwareinventoryview'; +-- Expected: MachineID, MachineName, Architecture, ProcessorCores, +-- TotalMemoryGB, AvailableMemoryGB, MemoryUsagePercent +``` + +--- + +## Schritt 7: WriteEnabled=false-Pfad / WriteEnabled=false Path + +```ini +[PgSqlDb] +WriteEnabled = false +``` + +```bash +dotnet run --project HarvesterWorkerService/HarvesterWorkerService.csproj +# Expected: Kein Fehler, keine PgSQL-Datensätze, SQLite/MongoDB laufen normal. +# Expected: No error, no PgSQL records, SQLite/MongoDB run normally. +``` + +SQL-Negativverifikation — bestätigt, dass tatsächlich KEINE Daten nach PostgreSQL geschrieben wurden: +/ SQL negative verification — confirms that NO data was written to PostgreSQL: + +```sql +-- Mit dem PostgreSQL-Server verbinden / Connect to the PostgreSQL server: +psql -h localhost -U inventar -d inventar + +-- Prüfen, dass keine Datensätze vorhanden sind / Verify no records were written: +SELECT COUNT(*) FROM Machines; -- Erwartet / Expected: 0 +SELECT COUNT(*) FROM HardwareInventories; -- Erwartet / Expected: 0 +SELECT COUNT(*) FROM SoftwareInventories; -- Erwartet / Expected: 0 +``` + +> Wenn `WriteEnabled = false` korrekt funktioniert, liefern alle drei Abfragen 0. +> Jeder Wert > 0 zeigt an, dass der `WriteEnabled`-Guard nicht greift. +> / If `WriteEnabled = false` works correctly, all three queries return 0. +> Any value > 0 indicates the `WriteEnabled` guard is not working. + +--- + +## Schritt 8: Unit-Tests & Coverage / Unit Tests & Coverage + +```bash +dotnet test InventarWorkerCommonTest/InventarWorkerCommonTest.csproj \ + --collect:"XPlat Code Coverage" \ + --results-directory ./TestResults + +# Coverage-Report generieren / Generate coverage report: +dotnet tool run reportgenerator \ + -reports:"./TestResults/**/coverage.cobertura.xml" \ + -targetdir:"./TestResults/CoverageReport" + +# CI-Gate: >=70% erforderlich, >=80% angestrebt +# CI gate: >=70% required, >=80% targeted +``` + +--- + +## Schritt 9: Abschluss-Validierung / Final Validation + +```bash +# Solution ohne Warnungen bauen / Build solution without warnings: +dotnet build InventarWorkerService.sln --no-incremental +# Expected: Build succeeded, 0 Warning(s) + +# Paket-Aktualität prüfen / Check package currency: +dotnet list package --outdated + +# DocFX generieren (nach API-Änderungen) / Generate DocFX (after API changes): +docfx docfx.json +``` diff --git a/specs/001-pgsql-paritaet/research.md b/specs/001-pgsql-paritaet/research.md new file mode 100644 index 0000000..cacef77 --- /dev/null +++ b/specs/001-pgsql-paritaet/research.md @@ -0,0 +1,244 @@ +# Research: PostgreSQL-Parität zum SqliteDbService + +**Branch**: `001-pgsql-paritaet` | **Date**: 2026-04-18 +**Input**: spec.md (clarified), existing codebase (PgSqlDbService.cs, SqliteDbService.cs, Initialize.cs, Worker.cs) + +--- + +## Entscheidungen / Decisions + +### R-01: INSERT mit RETURNING Id (statt last_insert_rowid()) + +**Decision**: `INSERT INTO Machines (...) VALUES (...) RETURNING Id` mit Dapper `QuerySingleAsync()`. + +**Rationale**: PostgreSQL kennt kein `last_insert_rowid()` (SQLite-spezifisch). `RETURNING Id` ist der +PostgreSQL-native Weg, die ID einer neu eingefügten Zeile zurückzugeben. Dapper unterstützt dieses +Muster nativ — `QuerySingleAsync()` mappt den zurückgegebenen Skalar direkt. + +**Alternatives considered**: +- `SELECT currval(pg_get_serial_sequence('Machines','Id'))` — umständlich und erfordert separate Abfrage +- `LASTVAL()` — session-spezifisch, aber thread-safety-Risiko bei Verbindungs-Pooling +- `RETURNING Id` — klarste und sicherste Option ✅ + +--- + +### R-02: DateTime.UtcNow für alle timestamptz-Spalten + +**Decision**: Alle `DateTime`-Werte werden als `DateTime.UtcNow` an Npgsql übergeben. + +**Rationale**: PostgreSQL `timestamptz` speichert intern immer UTC und konvertiert beim Auslesen in die +Session-Zeitzone. Npgsql 6+ erfordert `DateTimeKind.Utc` für `timestamptz`-Parameter, sonst Exception +`Cannot write DateTime with Kind=Local to PostgreSQL type 'timestamptz'`. Der bestehende SQLite-Code +verwendet bereits `DateTime.UtcNow` — Parität ist hier direkt möglich. + +**Alternatives considered**: +- `DateTimeOffset.UtcNow` — wäre korrekt, erfordert aber Modell-Anpassungen +- Timezone-Konvertierung per Npgsql-TypeMapping — unnötige Komplexität +- `DateTime.UtcNow` — einfachste, bereits etablierte Lösung im Codebase ✅ + +--- + +### R-03: WriteEnabled=null-Pattern für ServiceContainer.PgSqlDbService + +**Decision**: `Initialize.Services(Settings settings)` gibt `null` als `PgSqlDbService` zurück, +wenn `settings.PgSqlDb.WriteEnabled == false`. `ServiceContainer.PgSqlDbService` wird zu `PgSqlDbService?` +(nullable). Der Worker prüft vor jedem PgSQL-Call auf `_pgSqlDbService != null`. + +**Rationale**: Der Worker hat nach der Initialisierung keinen Zugriff mehr auf die settings-Variable. +Ein separates `WriteEnabled`-Flag im Worker würde die Verantwortlichkeiten mischen. Das Null-Pattern +ist idiomatisch in C# und erfordert keine neuen Felder oder Properties. + +**Impact on ServiceContainer**: +- `PgSqlDbService PgSqlDbService { get; }` → `PgSqlDbService? PgSqlDbService { get; }` +- Constructor: ArgumentNullException für PgSqlDbService entfernen (null ist erlaubt) +- Dispose-Methoden: null-Check vor Dispose von PgSqlDbService + +**Alternatives considered**: +- Separates `bool PgSqlWriteEnabled`-Property im ServiceContainer — unnötige Duplizierung +- Exception bei `WriteEnabled=false` — falsches Fehler-Modell für eine opt-in-Konfiguration +- Null-Return-Pattern ✅ + +--- + +### R-04: Fallback-Pfad überspringt PostgreSQL vollständig + +**Decision**: `Initialize.Services()` (parameterloser Overload, kein Settings-File) überspringt +PostgreSQL-Initialisierung komplett und setzt `PgSqlDbService = null`. + +**Rationale**: Ohne Settings-File gibt es keine Zugangsdaten für PostgreSQL. Der parameterlose Overload +baut Connection Strings mit Defaults (localhost, Port 5432, DB "inventar") — ohne Username/Password +würde die Verbindung in den meisten Produktivumgebungen scheitern. Der Fallback-Pfad ist für +unkonfigurierte Umgebungen gedacht, in denen PostgreSQL nicht verfügbar ist. + +**Alternatives considered**: +- Verbindungsversuch mit Defaults, bei Fehler graceful degrade — schlechte UX (verborgene Fehler) +- Exception im parameterlosem Overload — bricht bestehende Logik +- PgSQL überspringen = konservativste und sicherste Lösung ✅ + +--- + +### R-05: Fail-hard beim Startup, wenn PgSQL konfiguriert aber nicht erreichbar + +**Decision**: Wenn `WriteEnabled=true` und Settings-File vorhanden, aber PgSQL-Server nicht +erreichbar, propagiert `InitializeDatabase()` die Npgsql-Exception. Der Service startet nicht. + +**Rationale**: Dieses Verhalten gibt dem Operator sofortiges Feedback über Fehlkonfiguration. +Ein graceful degrade würde die Fehlkonfiguration verschleiern — Daten würden stilleherweise nur +in SQLite/MongoDB landen, ohne dass der Operator weiß, dass PgSQL-Writes fehlschlagen. +Fail-fast ist konsistent mit dem Prinzip "fail loudly on misconfiguration". + +**Alternatives considered**: +- Graceful degrade mit Log-Warnung — Konfigurationsfehler werden nicht sofort sichtbar +- Exception + automatischer Retry-Mechanismus — zu komplex für MVP +- Exception propagiert = sauberste Lösung ✅ + +--- + +### R-06: DROP VIEW IF EXISTS hardware_inventory_view in InitializeDatabase() + +**Decision**: `InitializeDatabase()` führt `DROP VIEW IF EXISTS hardware_inventory_view` aus, +bevor `CREATE OR REPLACE VIEW HardwareInventoryView AS ...` ausgeführt wird. + +**Rationale**: `CREATE OR REPLACE VIEW` ersetzt nur Views mit demselben Namen. Die alte View heißt +`hardware_inventory_view` (snake_case), die neue `HardwareInventoryView` (PascalCase). Ein einfaches +`CREATE OR REPLACE` würde die alte View nicht entfernen. `DROP VIEW IF EXISTS` ist idempotent — bei +frischen Installationen ohne alte View ist der Befehl ein No-op. + +**Alternatives considered**: +- Manuelle Migration durch Operator — schlechte DevEx, fehleranfällig +- Beide Views parallel beibehalten — Konfusion und Naming-Inkonsistenz +- DROP IF EXISTS + CREATE = idempotente, sichere Lösung ✅ + +--- + +### R-07: CleanupOldRecords(daysToKeep=0) ohne Mindestgrenze erlaubt + +**Decision**: `daysToKeep=0` ist gültiger Eingabewert. Cutoff = `DateTime.UtcNow.AddDays(0)` = jetzt. +Alle Einträge (CreatedAt < jetzt) werden gelöscht. Kein Minimum wird erzwungen. + +**Rationale**: Der Aufrufer trägt die Verantwortung für den gewählten Wert. Datenbankwartungs- +operationen müssen deterministische Semantik haben — eine hidden Mindestgrenze wäre überraschend. +Der Wert 0 als "alles löschen" ist eine bewusste, legitime Entscheidung des Aufrufers (z. B. für +Test-Cleanup oder Migrations-Szenarien). + +**Randfall negative Werte / Edge case negative values**: `daysToKeep = -5` ergibt +`DateTime.UtcNow.AddDays(-(-5)) = DateTime.UtcNow.AddDays(5)` (Cutoff 5 Tage in der Zukunft). +`DELETE WHERE CreatedAt < cutoff_zukunft` löscht ALLE Einträge, da alle `CreatedAt`-Werte in +der Vergangenheit liegen. Negative Werte verhalten sich damit wie `daysToKeep = 0`. Dies ist +mathematisch korrekt und dokumentiertes Verhalten — kein Sonderfall, keine Ausnahme. Der Aufrufer +ist für die Wahl des Wertes verantwortlich. +/ `daysToKeep = -5` results in `DateTime.UtcNow.AddDays(5)` as cutoff (5 days in the future). +`DELETE WHERE CreatedAt < future_cutoff` removes ALL records since all `CreatedAt` values are +in the past. Negative values behave equivalently to `daysToKeep = 0`. Mathematically correct; +no special handling needed; caller is responsible for the chosen value. + +**Alternatives considered**: +- `ArgumentOutOfRangeException` bei daysToKeep < 1 — bricht legitime Use Cases +- `Math.Max(1, daysToKeep)` — verschleiert Kaller-Intent +- Keine Einschränkung = konsistenteste Lösung ✅ + +--- + +### R-08: Test-Strategie für PgSqlDbService + +**Decision**: Zweistufige Test-Strategie: + +1. **Unit-Tests** (MSTest, kein externer Service): + - Testen: null-Check-Logik, ArgumentNullException-Verhalten, Fallback-Pfade in Initialize.cs + - Werkzeug: SQLite in-memory wo SQL-kompatibel (`Data Source=:memory:`), Fake-ServiceContainer + - Einschränkung: PostgreSQL-spezifische SQL-Syntax (RETURNING, DISTINCT ON) kann nicht per + SQLite-Unit-Test validiert werden + +2. **Integrationstests** (MSTest + `[TestCategory("Integration")]`): + - Testen: alle 21 Methoden end-to-end gegen echte PostgreSQL-Instanz + - Voraussetzung: lokale oder CI-PostgreSQL-Instanz (Verbindungsstring via Umgebungsvariable) + - Skip in normaler CI-Pipeline; explizit ausführbar via `dotnet test --filter TestCategory=Integration` + - Ausschluss aus normalem CI: `dotnet test --filter "TestCategory!=Integration"` + +**Trennlinie Unit ↔ Integration / Dividing line unit ↔ integration**: +- Unit-testbar (kein PostgreSQL-SQL): null-Checks in `ServiceContainer`, `ArgumentNullException`- + Logik in `SaveOrUpdateMachineAsync`, `FileNotFoundException`-Check in + `InitializeMachinesFromCsvAsync`, `CleanupOldRecordsAsync`-Cutoff-Berechnung, + `WriteEnabled`-Fallback-Pfade in `Initialize.Services()`. +- Integration-Test erforderlich (PostgreSQL-SQL): alle 21 DB-Methoden, die tatsächlich SQL + ausführen (RETURNING Id, DISTINCT ON, timestamptz-Vergleiche, VIEW-Abfragen). + Coverage-Gate ≥70% setzt Integration-Tests voraus — Unit-Tests allein decken zu wenig ab. +/ Unit-testable (no PostgreSQL SQL): null checks in `ServiceContainer`, `ArgumentNullException` + logic, `FileNotFoundException` check, cleanup cutoff calculation, `WriteEnabled` fallback. + Integration required (PostgreSQL SQL): all 21 DB methods that execute actual SQL. + +**Rationale**: Die Dapper-Pattern und SQL-Queries können nicht sinnvoll ohne echte DB geprüft werden, +da PostgreSQL-spezifische Syntax (RETURNING Id, DISTINCT ON, timestamptz) von SQLite nicht unterstützt +wird. Integrationstests mit echter PostgreSQL sind für vollständige Methodenabdeckung notwendig. + +**Alternatives considered**: +- Nur Unit-Tests mit Mocks (Moq auf IDbConnection) — zu aufwendig für 21 Methoden, fragile +- Nur Integrationstests — schließt CI ohne PostgreSQL aus +- Zweistufige Strategie (Unit + optional Integration) = pragmatischster Ansatz ✅ + +--- + +### R-09: CSV-Import mit NpgsqlTransaction + +**Decision**: `InitializeMachinesFromCsvAsync` verwendet `NpgsqlConnection.BeginTransaction()` und +übergibt die Transaktion an alle Dapper-Calls via `transaction`-Parameter. + +**Rationale**: Identisches Pattern wie SQLite-Implementierung (`SqliteConnection.BeginTransaction()`). +Npgsql unterstützt dasselbe ADO.NET-Transaktionsmodell. Bei Exception: `transaction.Rollback()` vor +`throw`. Bei Erfolg: `transaction.Commit()`. + +**Alternatives considered**: +- `TransactionScope` — funktioniert mit Npgsql, aber erfordert `using System.Transactions` und + verteilte Transaktionen; unnötige Komplexität +- Kein Transaktionsschutz — verletzt FR-010 und SC-004 +- `BeginTransaction()` = direktes Parity-Pattern zur SQLite-Implementierung ✅ + +--- + +### R-10: Vorhandene SQL Views in PgSqlDbService.InitializeDatabase() + +**Decision**: Die Statistik-Views (`ComputerModelStatisticsView`, `ArchitectureStatisticsView`, +`ModelArchitectureStatisticsView`, `HardwareStatisticsOverview`) sind bereits im SQL-Block von +`InitializeDatabase()` definiert. C# Lesemethoden für diese Views werden in einem separaten +Lastenheft implementiert (nicht in diesem Feature). + +**Rationale**: Die View-Definitionen sind bereits vorhanden — kein Änderungsbedarf an `InitializeDatabase()`. +Die C# Read-Methoden sind per Assumption aus spec.md explizit ausgeklammert. + +**Alternatives considered**: +- C# Lesemethoden in diesem Feature mitliefern — würde den Scope erheblich erweitern; kein Bedarf für laufenden HarvesterWorkerService-Betrieb +- Statistik-Views aus `InitializeDatabase()` entfernen — würde Schema-Fragmentierung erzeugen; Views gehören logisch zur DB-Initialisierung +- Views als eigenes Artefakt in einem Migration-Skript — unnötige Komplexität; `CREATE OR REPLACE VIEW` in `InitializeDatabase()` ist idempotent ✅ + +--- + +### R-11: SQLite als führende Datenbank — ID-Synchronisierung mit PostgreSQL + +**Decision**: SQLite ist die autoritäre Datenbank. Vor dem Aufruf von +`PgSqlDbService.SaveOrUpdateMachineAsync` setzt der Worker `machine.Id = _machineId` (SQLite-Id). +Die PgSQL-Implementierung führt einen INSERT mit expliziter Id durch. `_machineId` aus SQLite +kann danach für alle FK-Referenzen in `HardwareInventories` und `SoftwareInventories` in +PostgreSQL verwendet werden. + +**Rationale**: PostgreSQL ist eine optionale, sekundäre Senke für Lernzwecke (Azubis sollen +einen echten SQL-Server kennenlernen). SQLite ist immer vorhanden und ist das einzige System, +das der `HarvesterWorkerService` aktiv verwaltet. Da beide DBs immer gemeinsam initialisiert +und betrieben werden, ist die Maschinen-ID in SQLite die kanonische Referenz. +`GENERATED BY DEFAULT AS IDENTITY` erlaubt explizite Id-Werte — "BY DEFAULT" bedeutet: die +Sequenz wird nur verwendet, wenn kein expliziter Wert angegeben wird. + +**Voraussetzung / Prerequisite**: Beide Datenbanken werden immer gemeinsam initialisiert und +betrieben. Ein unabhängiges Zurücksetzen von PostgreSQL (ohne entsprechendes Reset von SQLite) +würde zu ID-Divergenz führen und muss als unsupported Szenario dokumentiert sein. + +**Alternatives considered**: +- Separate `pgMachineId`-Variable im Worker — unabhängige IDs, mehr Worker-Code, kein SQLite-Führungs-Konzept +- Keine explizite Id-Übergabe (PgSQL auto-assign) — IDs divergieren bei unterschiedlicher Initialisierung +- ID-Sync über expliziten INSERT = einfachster Ansatz für das "SQLite is master"-Modell ✅ + +--- + +## Offene Punkte / Open Points + +Keine. Alle NEEDS CLARIFICATION-Marker aus spec.md sind aufgelöst. +Alle research-Entscheidungen sind dokumentiert und direkt in data-model.md und contracts/ übertragen. diff --git a/specs/001-pgsql-paritaet/spec.md b/specs/001-pgsql-paritaet/spec.md new file mode 100644 index 0000000..272f116 --- /dev/null +++ b/specs/001-pgsql-paritaet/spec.md @@ -0,0 +1,283 @@ +# Feature Specification: PostgreSQL-Parität zum SqliteDbService + +**Feature Branch**: `001-pgsql-paritaet` +**Created**: 2026-04-18 +**Status**: Draft +**Input**: Lastenheft_PostgreSQL_Implementation.md (Review-Entscheidungen eingearbeitet, Stand 2026-04-18) + +--- + +## Clarifications + +### Session 2026-04-18 (Runde 1) + +- Q: Soll ein PostgreSQL-Write-Fehler während des Ernte-Zyklus isoliert werden (SQLite läuft weiter) oder als harter Fehler behandelt werden? → A: Isoliert — Fehler wird per `HandleException` geloggt, SQLite/MongoDB-Writes laufen für die aktuelle Maschine normal weiter. +- Q: Wenn PostgreSQL konfiguriert und `WriteEnabled=true`, aber der Server beim Startup nicht erreichbar ist — Exception propagieren (Fail hard) oder graceful degrade? → A: Fail hard — Exception propagiert, Service startet nicht; gibt dem Operator sofortiges Feedback über Fehlkonfiguration. +- Q: Soll `CleanupOldRecordsAsync(daysToKeep=0)` erlaubt sein (alle Einträge löschen) oder durch ein Minimum blockiert werden? → A: Erlaubt — `daysToKeep=0` löscht alle Hardware- und Software-Inventareinträge; kein Mindestwert wird erzwungen. + +### Session 2026-04-18 (Runde 2) + +- Q: Wie greift Worker.cs auf `PgSqlDb.WriteEnabled` zur Schreibzeit zu, wenn `settings` nach der Initialisierung verworfen ist? → A: `Initialize.Services()` gibt `null` zurück, wenn `WriteEnabled=false` — Worker braucht nur den Null-Check, kein separates Flag-Feld. +- Q: Soll `InitializeDatabase()` die alte View `hardware_inventory_view` explizit droppen, damit bei bestehenden DBs keine verwaisten Views zurückbleiben? → A: Ja — `DROP VIEW IF EXISTS hardware_inventory_view` in `InitializeDatabase()` einfügen; idempotent durch `IF EXISTS`. + +--- + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Inventardaten in PostgreSQL schreiben (Priority: P1) + +Als **HarvesterWorkerService** möchte ich Maschinen-, Hardware- und Software-Inventardaten +in PostgreSQL speichern können, damit eine vollständige relationale Kopie der Inventardaten +neben SQLite und MongoDB vorhanden ist. + +**Why this priority**: Ohne die Schreibmethoden ist PostgreSQL vollständig nutzlos. Alle +weiteren User Stories bauen auf funktionierenden Schreiboperationen auf. Diese Story liefert +den direkten Geschäftswert: ein zweiter, vollfunktionaler Datenbankprovider. + +**Independent Test**: Kann unabhängig getestet werden, indem `SaveOrUpdateMachineAsync`, +`SaveHardwareInventoryAsync` und `SaveSoftwareInventoryAsync` gegen eine lokale +PostgreSQL-Instanz aufgerufen werden und die Daten anschliessend per direkter SQL-Abfrage +verifiziert werden. + +**Acceptance Scenarios**: + +1. **Given** eine konfigurierte PostgreSQL-Instanz und ein `PgSqlDbService`, + **When** `SaveOrUpdateMachineAsync(machine)` für eine neue Maschine aufgerufen wird, + **Then** existiert ein Datensatz mit korrektem Namen in der Tabelle `Machines`. + +2. **Given** eine Maschine mit bekanntem Namen bereits in PostgreSQL vorhanden, + **When** `SaveOrUpdateMachineAsync(machine)` erneut für denselben Namen aufgerufen wird, + **Then** wird der bestehende Datensatz aktualisiert (kein Duplikat). + +3. **Given** eine Maschine mit bekanntem Namen, + **When** `SaveOrUpdateMachineAsync(machine, isHarvester: true)` aufgerufen wird, + **Then** werden auch `IPv4`, `IPv6`, `FQDN` und `LastHarvested` aktualisiert. + +4. **Given** eine `machineId` und ein `HardwareInventory`-Objekt, + **When** `SaveHardwareInventoryAsync(machineId, hardware)` aufgerufen wird, + **Then** ist ein Datensatz in `HardwareInventories` mit korrekten Werten vorhanden. + +5. **Given** eine `machineId` und ein `SoftwareInventory`-Objekt, + **When** `SaveSoftwareInventoryAsync(machineId, software)` aufgerufen wird, + **Then** sind alle JSON-Felder (Prozesse, Software, Services, Umgebung, Autostart, Runtime) + korrekt serialisiert in `SoftwareInventories` gespeichert. + +--- + +### User Story 2 - HarvesterWorkerService schreibt parallel nach PostgreSQL (Priority: P2) + +Als **Systemadministrator** möchte ich, dass der `HarvesterWorkerService` bei aktiviertem +`WriteEnabled` automatisch Inventardaten nach PostgreSQL schreibt — zusätzlich zu SQLite und +MongoDB — damit PostgreSQL als vollwertiger dritter Persistenz-Provider genutzt werden kann. + +**Why this priority**: Ohne Worker-Integration kann die PgSQL-Implementierung nicht +end-to-end getestet werden. Der `WriteEnabled`-Schutzschalter ist ausserdem wichtig, um +unkonfigurierte Produktivumgebungen zu schützen. + +**Independent Test**: Kann getestet werden, indem `PgSqlDb.WriteEnabled = true` in der +Settings-Datei gesetzt und der `HarvesterWorkerService` gestartet wird. Danach müssen in +PostgreSQL Datensätze in `Machines`, `HardwareInventories` und `SoftwareInventories` +erscheinen. + +**Acceptance Scenarios**: + +1. **Given** `PgSqlDb.WriteEnabled = true` in den Settings, + **When** der `HarvesterWorkerService` einen Ernte-Zyklus durchführt, + **Then** werden Maschinen-, Hardware- und Software-Daten parallel in SQLite, MongoDB + **und** PostgreSQL gespeichert. + +2. **Given** `PgSqlDb.WriteEnabled = false` in den Settings, + **When** `Initialize.Services(settings)` aufgerufen wird, + **Then** ist `ServiceContainer.PgSqlDbService` `null`; der Worker führt keine PostgreSQL-Schreibzugriffe durch und wirft keine Exception. + +3. **Given** keine Settings-Datei vorhanden (Fallback-Pfad), + **When** `Initialize.Services()` aufgerufen wird, + **Then** wird die PostgreSQL-Initialisierung übersprungen; `ServiceContainer.PgSqlDbService` + ist `null` und keine Exception wird geworfen. + +--- + +### User Story 3 - Inventardaten aus PostgreSQL lesen (Priority: P3) + +Als **Entwickler** möchte ich alle Lese-, Lookup- und Wartungs-Methoden des `PgSqlDbService` +nutzen können, die auch im `SqliteDbService` vorhanden sind, damit ich den Provider +transparent wechseln oder parallel betreiben kann. + +**Why this priority**: Lesemethoden werden für den laufenden HarvesterWorkerService-Betrieb +nicht unmittelbar benötigt (der Worker liest aus SQLite), sind aber für vollständige +API-Parität und künftiges Provider-Switching erforderlich. + +**Independent Test**: Kann getestet werden, indem Testdaten per Schreibmethoden eingefügt +und anschliessend per Lesemethode abgerufen werden. Signaturen müssen identisch zur SQLite-API +sein. + +**Acceptance Scenarios**: + +1. **Given** mehrere Maschinen in PostgreSQL (aktiv, deaktiviert, deprovisioniert), + **When** `GetAllActiveMachinesAsync()` aufgerufen wird, + **Then** werden ausschliesslich aktive Maschinen (`Disabled=0, Deprovisioned=0`) zurückgegeben. + +2. **Given** eine Maschine mit `Id = 5` in PostgreSQL, + **When** `GetMachineByIdAsync(5)` aufgerufen wird, + **Then** wird die korrekte Maschine zurückgegeben. + +3. **Given** eine Maschine mit mehreren Hardware-Inventareinträgen, + **When** `GetLatestHardwareInventoryAsync(machineId)` aufgerufen wird, + **Then** wird nur der zeitlich neueste Eintrag zurückgegeben. + +4. **Given** `GetAllActiveMachinesWithNetworkInfoAsync()` wird aufgerufen, + **Then** werden nur Maschinen zurückgegeben, die mindestens einen nicht-leeren + Netzwerkwert (IPv4, IPv6 oder FQDN) haben. + +5. **Given** Inventardaten vorhanden, + **When** `GetMachineCountAsync()`, `GetHardwareInventoryCountAsync()`, + `GetSoftwareInventoryCountAsync()` aufgerufen werden, + **Then** stimmen die Ergebnisse mit den tatsächlichen Datenbankeinträgen überein. + +--- + +### User Story 4 - Maschinen per CSV importieren (Priority: P4) + +Als **Systemadministrator** möchte ich eine Liste von Maschinen per CSV-Datei in PostgreSQL +importieren können, damit der Initialbestand ohne manuellen Datenbankzugriff befüllt +werden kann. + +**Why this priority**: Der CSV-Import ist für den Erstbetrieb relevant, aber keine +laufende Betriebsfunktion. Er kann nach den Kern-CRUD-Methoden umgesetzt werden. + +**Independent Test**: Kann getestet werden, indem eine CSV-Datei im bekannten Format erstellt +und `InitializeMachinesFromCsvAsync(csvFilePath)` aufgerufen wird. Die importierten Maschinen +müssen danach per `GetMachinesAsync()` abrufbar sein. + +**Acceptance Scenarios**: + +1. **Given** eine gültige CSV-Datei mit 3 Maschinen, + **When** `InitializeMachinesFromCsvAsync(path)` aufgerufen wird, + **Then** werden 3 neue Maschinen in PostgreSQL angelegt und die Anzahl importierter + Datensätze zurückgegeben. + +2. **Given** eine CSV-Datei, bei der eine Maschine bereits in der Datenbank existiert, + **When** `InitializeMachinesFromCsvAsync(path)` aufgerufen wird, + **Then** wird die bestehende Maschine nicht überschrieben (nur neue werden importiert). + +3. **Given** ein Fehler während des Imports, + **When** eine Exception auftritt, + **Then** wird die Transaktion vollständig zurückgerollt (kein Teilergebnis in der DB). + +4. **Given** eine nicht vorhandene CSV-Datei, + **When** `InitializeMachinesFromCsvAsync(path)` aufgerufen wird, + **Then** wird eine `FileNotFoundException` geworfen. + +--- + +### User Story 5 - View-Namen auf PascalCase vereinheitlichen (Priority: P5) + +Als **Entwickler** möchte ich, dass alle View-Namen und Spaltenaliase in PostgreSQL mit +den SQLite-Konventionen (PascalCase) übereinstimmen, damit dieselben Abfragen ohne +Anpassung gegen beide Provider funktionieren. + +**Why this priority**: Technische Voraussetzung für transparente Parität. Ohne einheitliche +Namen würden identische Abfragen gegen verschiedene Provider fehlschlagen. + +**Independent Test**: Kann geprüft werden, indem `SELECT * FROM HardwareInventoryView LIMIT 1` +gegen die PostgreSQL-Instanz ausgeführt wird und PascalCase-Spalten zurückgegeben werden. + +**Acceptance Scenarios**: + +1. **Given** die initialisierte PostgreSQL-Datenbank, + **When** `SELECT * FROM HardwareInventoryView LIMIT 1` ausgeführt wird, + **Then** ist die View unter `HardwareInventoryView` vorhanden (nicht `hardware_inventory_view`). + +2. **Given** die View `HardwareInventoryView`, + **When** die Spaltennamen abgefragt werden, + **Then** lauten sie `MachineID`, `MachineName`, `Architecture`, `ProcessorCores`, + `TotalMemoryGB`, `AvailableMemoryGB`, `MemoryUsagePercent` (PascalCase, identisch zu SQLite). + +3. **Given** eine Datenbank, die zuvor mit `hardware_inventory_view` initialisiert wurde, + **When** `InitializeDatabase()` erneut aufgerufen wird, + **Then** existiert `hardware_inventory_view` nicht mehr; nur `HardwareInventoryView` ist vorhanden. + +--- + +### Edge Cases + +- Was passiert, wenn der PostgreSQL-Server beim Startup nicht erreichbar ist (Settings vorhanden, `WriteEnabled=true`)? + → Exception propagiert, Service startet nicht (Fail hard). Gibt dem Operator sofortiges Feedback über Fehlkonfiguration. Nur wenn keine Settings-Datei vorhanden ist, wird PgSQL übersprungen (FR-014). +- Was passiert, wenn `SaveOrUpdateMachineAsync` mit einem `null`-Maschinenobjekt aufgerufen wird? + → `ArgumentNullException` erwartet. +- Was passiert, wenn `CleanupOldRecordsAsync` mit `daysToKeep=0` aufgerufen wird? + → Erlaubt: alle Hardware- und Software-Inventareinträge werden gelöscht. Kein Mindestwert wird erzwungen; der Aufrufer trägt die Verantwortung. +- Was passiert bei Zeitzonenunterschieden? PostgreSQL `timestamptz` erwartet UTC. + → Alle `DateTime`-Werte MÜSSEN als `DateTime.UtcNow` übergeben werden. +- Was passiert, wenn `PgSqlDbService` im `ServiceContainer` `null` ist (kein Settings-File)? + → Worker muss vor jedem Aufruf auf `null` prüfen. + +--- + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: `PgSqlDbService` MUSS `SaveOrUpdateMachineAsync(Machine, bool isHarvester)` implementieren — Parität zu `SqliteDbService`. +- **FR-002**: `PgSqlDbService` MUSS `SaveHardwareInventoryAsync(int machineId, HardwareInventory)` implementieren. +- **FR-003**: `PgSqlDbService` MUSS `SaveSoftwareInventoryAsync(int machineId, SoftwareInventory)` mit JSON-Serialisierung implementieren. +- **FR-004**: `PgSqlDbService` MUSS `GetMachinesAsync()`, `GetAllActiveMachinesAsync()`, `GetAllActiveMachinesWithNetworkInfoAsync()`, `GetAllDisabledMachinesAsync()`, `GetAllDeprovisionedMachinesAsync()` implementieren. +- **FR-005**: `PgSqlDbService` MUSS `GetMachineByIdAsync(int)` und `GetMachineByNameAsync(string)` implementieren. +- **FR-006**: `PgSqlDbService` MUSS `GetLatestHardwareInventoryAsync(int)` und `GetLatestSoftwareInventoryAsync(int)` implementieren. +- **FR-007**: `PgSqlDbService` MUSS `CleanupOldRecordsAsync(int daysToKeep = 30)` implementieren. +- **FR-008**: `PgSqlDbService` MUSS `HasMachineRecordsAsync()`, `HasHardwareInventoryRecordsAsync()`, `HasSoftwareInventoryRecordsAsync()` implementieren. +- **FR-009**: `PgSqlDbService` MUSS `GetMachineCountAsync()`, `GetHardwareInventoryCountAsync()`, `GetSoftwareInventoryCountAsync()` implementieren. +- **FR-010**: `PgSqlDbService` MUSS `InitializeMachinesFromCsvAsync(string csvFilePath)` mit Transaktionsschutz implementieren. +- **FR-011**: PostgreSQL-INSERT MUSS `RETURNING Id` statt `last_insert_rowid()` verwenden. +- **FR-012**: Alle `DateTime`-Werte MÜSSEN als `DateTime.UtcNow` an PostgreSQL übergeben werden. +- **FR-013**: Die View `hardware_inventory_view` MUSS in `HardwareInventoryView` umbenannt und Spaltenaliase auf PascalCase angepasst werden. `InitializeDatabase()` MUSS `DROP VIEW IF EXISTS hardware_inventory_view` ausführen, bevor die neue View angelegt wird, um bei bestehenden Datenbanken keine verwaisten Views zu hinterlassen. +- **FR-014**: `Initialize.Services()` MUSS `null` als `PgSqlDbService` zurückgeben, wenn (a) keine Settings-Datei vorhanden ist oder (b) `PgSqlDb.WriteEnabled = false`. Der Worker prüft ausschliesslich auf `null` — kein separates WriteEnabled-Flag im Worker erforderlich. +- **FR-015**: `HarvesterWorkerService/Worker.cs` MUSS PostgreSQL-Schreiboperationen aufrufen, wenn `PgSqlDbService != null`. Ein Fehler beim PostgreSQL-Write MUSS isoliert werden: per `HandleException` loggen, SQLite/MongoDB-Writes der aktuellen Maschine laufen normal weiter. +- **FR-016**: `InitializeDatabase()` bleibt synchron (Parität zu `SqliteDbService`; keine async-Version). +- **FR-017**: Alle öffentlichen Methoden von `PgSqlDbService` MÜSSEN vollständige XML-Dokumentation haben (zweisprachig DE/EN, CEFR B2). + +--- + +## Constitution Alignment *(mandatory)* + +- **CA-001 Branching**: Feature wird auf Branch `001-pgsql-paritaet` implementiert, Merge via PR zu `main`. +- **CA-002 Toolchain**: Betrifft ausschliesslich `InventarWorkerCommon` und `HarvesterWorkerService`; beide sind bereits auf .NET 10 / C# 14.0. Kein Upgrade erforderlich. +- **CA-003 Dependency Currency**: `Npgsql`, `Dapper`, `CsvHelper` auf aktuelle stabile Versionen prüfen. Keine neuen Packages erforderlich. +- **CA-004 Coverage**: Neue Methoden in `PgSqlDbService` benötigen Unit-Tests. CI-Gate ≥70%, Ziel ≥80%. Integrationstests gegen echte PostgreSQL-Instanz werden mit Skip-Attribut oder separatem Profil markiert. +- **CA-005 Layering**: Alle Änderungen in `InventarWorkerCommon/Services/Database/PgSqlDbService.cs` und `InventarWorkerCommon/Services/Common/Initialize.cs`. Worker-Anpassungen in `HarvesterWorkerService/Worker.cs`. +- **CA-006 Linguistic Rules**: XML-Dokumentation zweisprachig (DE zuerst, EN als zweite Sprache), CEFR B2. +- **CA-007 Documentation Enforcement**: `docfx docfx.json` muss nach Abschluss aller API-Änderungen ausgeführt werden. +- **CA-008 Testing Impact**: Red-Green-Refactor für alle 21 neuen Methoden. Unit-Tests sind unabhängig von echter DB (gemockte Verbindung oder In-Memory-Pattern). +- **CA-009 Data Contracts**: `System.Text.Json` für JSON-Serialisierung. Dapper mit expliziten SQL-Strings. PascalCase Tabellen/Spalten-Konvention. View-Umbenennung ist Pflicht. + +### Key Entities + +- **Machine**: Stammdaten einer verwalteten Maschine (Name, Betriebssystem, Netzwerkinfos, Status-Flags `Disabled`/`Deprovisioned`, Zeitstempel). +- **HardwareInventory**: Hardware-Momentaufnahme einer Maschine (CPU-Details, Speicherwerte, Architektur, Computerhersteller/-modell). +- **SoftwareInventory**: Software-Momentaufnahme als JSON-serialisierte Teillisten (laufende Prozesse, installierte Software, Windows-Dienste, Umgebungsvariablen, Autostart-Programme, Runtime-Info). +- **MachineState**: Projektion der `Machines`-Tabelle für View-Abfragen (Id, Name, Netzwerkinfos, Status-Flags). Wird von allen View-basierten Lesemethoden zurückgegeben. + +--- + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: Alle 21 öffentlichen Methoden des `SqliteDbService` sind im `PgSqlDbService` mit identischen Signaturen implementiert und kompilieren ohne Warnungen. +- **SC-002**: Ein kompletter Ernte-Zyklus des `HarvesterWorkerService` mit `WriteEnabled=true` schreibt nachweislich Daten in PostgreSQL (verifizierbar per direkter SQL-Abfrage auf alle drei Tabellen). +- **SC-003**: Alle Pfade mit `WriteEnabled=false` oder fehlendem Settings-File laufen ohne Exceptions oder Fehlermeldungen durch. +- **SC-004**: Der CSV-Import überträgt eine 10-Einträge-Datei vollständig; bei simuliertem Abbruch nach 5 Einträgen sind 0 Einträge in der Datenbank (Transaktionsschutz verifiziert). +- **SC-005**: CI-Coverage für neue Code-Pfade liegt bei ≥70%; Zielwert ≥80%. +- **SC-006**: `SELECT * FROM HardwareInventoryView` liefert PascalCase-Spalten identisch zur SQLite-View. +- **SC-007**: `dotnet build InventarWorkerService.sln` läuft ohne Warnungen bezogen auf den neuen Code. + +--- + +## Assumptions + +- PostgreSQL-Version 14 oder höher (unterstützt `GENERATED BY DEFAULT AS IDENTITY`, `RETURNING Id`, `timestamptz`). +- Eine laufende PostgreSQL-Instanz ist für Integrationstests verfügbar; Unit-Tests laufen ohne echte DB. +- Die bestehenden SQLite-Bugs (`GetAllDeprovisionedMachinesAsync`, `GetAllDisabledMachinesAsync`) werden in einem separaten Feature (`Lastenheft_SQLite_ViewQuery_Bugfix.md`) behoben — nicht Teil dieser Spezifikation. +- Ein formales `IDbService`-Interface wird in diesem Feature nicht eingeführt (separates Lastenheft). +- Lesemethoden für Statistik-Views (`ComputerModelStatisticsView` etc.) sind nicht Teil dieses Features (separates Lastenheft). +- MongoDB-Parität ist nicht Teil dieses Features (separates Lastenheft). From ba5a2b80723be25dbd833b262ff39a9d983b13fd Mon Sep 17 00:00:00 2001 From: Thorsten Hindermann Date: Sun, 19 Apr 2026 01:01:09 +0200 Subject: [PATCH 2/7] Update Spec-Kit specify and git integrations --- .agents/skills/speckit-analyze/SKILL.md | 71 ++- .agents/skills/speckit-checklist/SKILL.md | 66 +++ .agents/skills/speckit-clarify/SKILL.md | 68 ++- .agents/skills/speckit-constitution/SKILL.md | 66 +++ .agents/skills/speckit-git-commit/SKILL.md | 53 ++ .agents/skills/speckit-git-feature/SKILL.md | 72 +++ .../skills/speckit-git-initialize/SKILL.md | 54 +++ .agents/skills/speckit-git-remote/SKILL.md | 50 ++ .agents/skills/speckit-git-validate/SKILL.md | 54 +++ .agents/skills/speckit-plan/SKILL.md | 10 +- .agents/skills/speckit-specify/SKILL.md | 71 ++- .agents/skills/speckit-taskstoissues/SKILL.md | 66 +++ .claude/skills/speckit-analyze/SKILL.md | 260 ++++++++++ .claude/skills/speckit-checklist/SKILL.md | 372 ++++++++++++++ .claude/skills/speckit-clarify/SKILL.md | 254 ++++++++++ .claude/skills/speckit-constitution/SKILL.md | 157 ++++++ .claude/skills/speckit-implement/SKILL.md | 209 ++++++++ .claude/skills/speckit-plan/SKILL.md | 152 ++++++ .claude/skills/speckit-specify/SKILL.md | 330 +++++++++++++ .claude/skills/speckit-tasks/SKILL.md | 202 ++++++++ .claude/skills/speckit-taskstoissues/SKILL.md | 106 ++++ .gemini/commands/speckit.analyze.toml | 79 ++- .gemini/commands/speckit.checklist.toml | 76 ++- .gemini/commands/speckit.clarify.toml | 78 ++- .gemini/commands/speckit.constitution.toml | 76 ++- .gemini/commands/speckit.git.commit.toml | 50 ++ .gemini/commands/speckit.git.feature.toml | 69 +++ .gemini/commands/speckit.git.initialize.toml | 51 ++ .gemini/commands/speckit.git.remote.toml | 47 ++ .gemini/commands/speckit.git.validate.toml | 51 ++ .gemini/commands/speckit.implement.toml | 71 ++- .gemini/commands/speckit.plan.toml | 89 +++- .gemini/commands/speckit.specify.toml | 161 +++++-- .gemini/commands/speckit.tasks.toml | 80 +++- .gemini/commands/speckit.taskstoissues.toml | 73 ++- .github/agents/speckit.analyze.agent.md | 71 ++- .github/agents/speckit.checklist.agent.md | 66 +++ .github/agents/speckit.clarify.agent.md | 68 ++- .github/agents/speckit.constitution.agent.md | 66 +++ .github/agents/speckit.git.commit.agent.md | 51 ++ .github/agents/speckit.git.feature.agent.md | 70 +++ .../agents/speckit.git.initialize.agent.md | 52 ++ .github/agents/speckit.git.remote.agent.md | 48 ++ .github/agents/speckit.git.validate.agent.md | 52 ++ .github/agents/speckit.implement.agent.md | 65 ++- .github/agents/speckit.plan.agent.md | 73 ++- .github/agents/speckit.specify.agent.md | 145 ++++-- .github/agents/speckit.tasks.agent.md | 63 +++ .github/agents/speckit.taskstoissues.agent.md | 66 +++ .github/copilot-instructions.md | 5 + .github/prompts/speckit.git.commit.prompt.md | 3 + .github/prompts/speckit.git.feature.prompt.md | 3 + .../prompts/speckit.git.initialize.prompt.md | 3 + .github/prompts/speckit.git.remote.prompt.md | 3 + .../prompts/speckit.git.validate.prompt.md | 3 + .opencode/command/speckit.analyze.md | 71 ++- .opencode/command/speckit.checklist.md | 66 +++ .opencode/command/speckit.clarify.md | 68 ++- .opencode/command/speckit.constitution.md | 66 +++ .opencode/command/speckit.git.commit.md | 51 ++ .opencode/command/speckit.git.feature.md | 70 +++ .opencode/command/speckit.git.initialize.md | 52 ++ .opencode/command/speckit.git.remote.md | 48 ++ .opencode/command/speckit.git.validate.md | 52 ++ .opencode/command/speckit.implement.md | 65 ++- .opencode/command/speckit.plan.md | 73 ++- .opencode/command/speckit.specify.md | 145 ++++-- .opencode/command/speckit.tasks.md | 63 +++ .opencode/command/speckit.taskstoissues.md | 66 +++ .specify/extensions.yml | 148 ++++++ .specify/extensions/.registry | 44 ++ .specify/extensions/git/README.md | 100 ++++ .../git/commands/speckit.git.commit.md | 48 ++ .../git/commands/speckit.git.feature.md | 67 +++ .../git/commands/speckit.git.initialize.md | 49 ++ .../git/commands/speckit.git.remote.md | 45 ++ .../git/commands/speckit.git.validate.md | 49 ++ .specify/extensions/git/config-template.yml | 62 +++ .specify/extensions/git/extension.yml | 140 ++++++ .specify/extensions/git/git-config.yml | 62 +++ .../git/scripts/bash/auto-commit.sh | 140 ++++++ .../git/scripts/bash/create-new-feature.sh | 453 ++++++++++++++++++ .../extensions/git/scripts/bash/git-common.sh | 54 +++ .../git/scripts/bash/initialize-repo.sh | 54 +++ .../git/scripts/powershell/auto-commit.ps1 | 169 +++++++ .../scripts/powershell/create-new-feature.ps1 | 403 ++++++++++++++++ .../git/scripts/powershell/git-common.ps1 | 51 ++ .../scripts/powershell/initialize-repo.ps1 | 69 +++ .specify/init-options.json | 10 + .specify/integration.json | 4 + .specify/integrations/claude.manifest.json | 16 + .specify/integrations/codex.manifest.json | 16 + .specify/integrations/copilot.manifest.json | 25 + .specify/integrations/gemini.manifest.json | 16 + .specify/integrations/opencode.manifest.json | 16 + .specify/integrations/speckit.manifest.json | 6 + .specify/workflows/speckit/workflow.yml | 63 +++ .specify/workflows/workflow-registry.json | 13 + AGENTS.md | 5 + CLAUDE.md | 5 + Directory.Build.props | 6 +- GEMINI.md | 5 + 102 files changed, 7865 insertions(+), 274 deletions(-) create mode 100644 .agents/skills/speckit-git-commit/SKILL.md create mode 100644 .agents/skills/speckit-git-feature/SKILL.md create mode 100644 .agents/skills/speckit-git-initialize/SKILL.md create mode 100644 .agents/skills/speckit-git-remote/SKILL.md create mode 100644 .agents/skills/speckit-git-validate/SKILL.md create mode 100644 .claude/skills/speckit-analyze/SKILL.md create mode 100644 .claude/skills/speckit-checklist/SKILL.md create mode 100644 .claude/skills/speckit-clarify/SKILL.md create mode 100644 .claude/skills/speckit-constitution/SKILL.md create mode 100644 .claude/skills/speckit-implement/SKILL.md create mode 100644 .claude/skills/speckit-plan/SKILL.md create mode 100644 .claude/skills/speckit-specify/SKILL.md create mode 100644 .claude/skills/speckit-tasks/SKILL.md create mode 100644 .claude/skills/speckit-taskstoissues/SKILL.md create mode 100644 .gemini/commands/speckit.git.commit.toml create mode 100644 .gemini/commands/speckit.git.feature.toml create mode 100644 .gemini/commands/speckit.git.initialize.toml create mode 100644 .gemini/commands/speckit.git.remote.toml create mode 100644 .gemini/commands/speckit.git.validate.toml create mode 100644 .github/agents/speckit.git.commit.agent.md create mode 100644 .github/agents/speckit.git.feature.agent.md create mode 100644 .github/agents/speckit.git.initialize.agent.md create mode 100644 .github/agents/speckit.git.remote.agent.md create mode 100644 .github/agents/speckit.git.validate.agent.md create mode 100644 .github/prompts/speckit.git.commit.prompt.md create mode 100644 .github/prompts/speckit.git.feature.prompt.md create mode 100644 .github/prompts/speckit.git.initialize.prompt.md create mode 100644 .github/prompts/speckit.git.remote.prompt.md create mode 100644 .github/prompts/speckit.git.validate.prompt.md create mode 100644 .opencode/command/speckit.git.commit.md create mode 100644 .opencode/command/speckit.git.feature.md create mode 100644 .opencode/command/speckit.git.initialize.md create mode 100644 .opencode/command/speckit.git.remote.md create mode 100644 .opencode/command/speckit.git.validate.md create mode 100644 .specify/extensions.yml create mode 100644 .specify/extensions/.registry create mode 100644 .specify/extensions/git/README.md create mode 100644 .specify/extensions/git/commands/speckit.git.commit.md create mode 100644 .specify/extensions/git/commands/speckit.git.feature.md create mode 100644 .specify/extensions/git/commands/speckit.git.initialize.md create mode 100644 .specify/extensions/git/commands/speckit.git.remote.md create mode 100644 .specify/extensions/git/commands/speckit.git.validate.md create mode 100644 .specify/extensions/git/config-template.yml create mode 100644 .specify/extensions/git/extension.yml create mode 100644 .specify/extensions/git/git-config.yml create mode 100755 .specify/extensions/git/scripts/bash/auto-commit.sh create mode 100755 .specify/extensions/git/scripts/bash/create-new-feature.sh create mode 100755 .specify/extensions/git/scripts/bash/git-common.sh create mode 100755 .specify/extensions/git/scripts/bash/initialize-repo.sh create mode 100644 .specify/extensions/git/scripts/powershell/auto-commit.ps1 create mode 100644 .specify/extensions/git/scripts/powershell/create-new-feature.ps1 create mode 100644 .specify/extensions/git/scripts/powershell/git-common.ps1 create mode 100644 .specify/extensions/git/scripts/powershell/initialize-repo.ps1 create mode 100644 .specify/init-options.json create mode 100644 .specify/integration.json create mode 100644 .specify/integrations/claude.manifest.json create mode 100644 .specify/integrations/codex.manifest.json create mode 100644 .specify/integrations/copilot.manifest.json create mode 100644 .specify/integrations/gemini.manifest.json create mode 100644 .specify/integrations/opencode.manifest.json create mode 100644 .specify/integrations/speckit.manifest.json create mode 100644 .specify/workflows/speckit/workflow.yml create mode 100644 .specify/workflows/workflow-registry.json diff --git a/.agents/skills/speckit-analyze/SKILL.md b/.agents/skills/speckit-analyze/SKILL.md index 6f962ea..80d6f32 100644 --- a/.agents/skills/speckit-analyze/SKILL.md +++ b/.agents/skills/speckit-analyze/SKILL.md @@ -16,6 +16,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before analysis)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_analyze` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Goal. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Goal Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit.tasks` has successfully produced a complete `tasks.md`. @@ -47,7 +81,7 @@ Load only the minimal necessary context from each artifact: - Overview/Context - Functional Requirements -- Non-Functional Requirements +- Success Criteria (measurable outcomes — e.g., performance, security, availability, user success, business impact) - User Stories - Edge Cases (if present) @@ -74,7 +108,7 @@ Load only the minimal necessary context from each artifact: Create internal representations (do not include raw artifacts in output): -- **Requirements inventory**: Each functional + non-functional requirement with a stable key (derive slug based on imperative phrase; e.g., "User can upload file" → `user-can-upload-file`) +- **Requirements inventory**: For each Functional Requirement (FR-###) and Success Criterion (SC-###), record a stable key. Use the explicit FR-/SC- identifier as the primary key when present, and optionally also derive an imperative-phrase slug for readability (e.g., "User can upload file" → `user-can-upload-file`). Include only Success Criteria items that require buildable work (e.g., load-testing infrastructure, security audit tooling), and exclude post-launch outcome metrics and business KPIs (e.g., "Reduce support tickets by 50%"). - **User story/action inventory**: Discrete user actions with acceptance criteria - **Task coverage mapping**: Map each task to one or more requirements or stories (inference by keyword / explicit reference patterns like IDs or key phrases) - **Constitution rule set**: Extract principle names and MUST/SHOULD normative statements @@ -108,7 +142,7 @@ Focus on high-signal findings. Limit to 50 findings total; aggregate remainder i - Requirements with zero associated tasks - Tasks with no mapped requirement/story -- Non-functional requirements not reflected in tasks (e.g., performance, security) +- Success Criteria requiring buildable work (performance, security, availability) not reflected in tasks #### F. Inconsistency @@ -168,6 +202,37 @@ At end of report, output a concise Next Actions block: Ask the user: "Would you like me to suggest concrete remediation edits for the top N issues?" (Do NOT apply them automatically.) +### 9. Check for extension hooks + +After reporting, check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.after_analyze` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Operating Principles ### Context Efficiency diff --git a/.agents/skills/speckit-checklist/SKILL.md b/.agents/skills/speckit-checklist/SKILL.md index 53bc73d..babd5a7 100644 --- a/.agents/skills/speckit-checklist/SKILL.md +++ b/.agents/skills/speckit-checklist/SKILL.md @@ -37,6 +37,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before checklist generation)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_checklist` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Execution Steps. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Execution Steps 1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS list. @@ -299,3 +333,35 @@ Sample items: - Correct: Validation of requirement quality - Wrong: "Does it do X?" - Correct: "Is X clearly specified?" + +## Post-Execution Checks + +**Check for extension hooks (after checklist generation)**: +Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.after_checklist` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/.agents/skills/speckit-clarify/SKILL.md b/.agents/skills/speckit-clarify/SKILL.md index 45ad312..7b1b1db 100644 --- a/.agents/skills/speckit-clarify/SKILL.md +++ b/.agents/skills/speckit-clarify/SKILL.md @@ -16,6 +16,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before clarification)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_clarify` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Outline. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Outline Goal: Detect and reduce ambiguity or missing decision points in the active feature specification and record the clarifications directly in the spec file. @@ -144,7 +178,7 @@ Execution steps: - Functional ambiguity → Update or add a bullet in Functional Requirements. - User interaction / actor distinction → Update User Stories or Actors subsection (if present) with clarified role, constraint, or scenario. - Data shape / entities → Update Data Model (add fields, types, relationships) preserving ordering; note added constraints succinctly. - - Non-functional constraint → Add/modify measurable criteria in Non-Functional / Quality Attributes section (convert vague adjective to metric or explicit target). + - Non-functional constraint → Add/modify measurable criteria in Success Criteria > Measurable Outcomes (convert vague adjective to metric or explicit target). - Edge case / negative flow → Add a new bullet under Edge Cases / Error Handling (or create such subsection if template provides placeholder for it). - Terminology conflict → Normalize term across spec; retain original only if necessary by adding `(formerly referred to as "X")` once. - If the clarification invalidates an earlier ambiguous statement, replace that statement instead of duplicating; leave no obsolete contradictory text. @@ -181,3 +215,35 @@ Behavior rules: - If quota reached with unresolved high-impact categories remaining, explicitly flag them under Deferred with rationale. Context for prioritization: $ARGUMENTS + +## Post-Execution Checks + +**Check for extension hooks (after clarification)**: +Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.after_clarify` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/.agents/skills/speckit-constitution/SKILL.md b/.agents/skills/speckit-constitution/SKILL.md index 23b2a17..3c56c2e 100644 --- a/.agents/skills/speckit-constitution/SKILL.md +++ b/.agents/skills/speckit-constitution/SKILL.md @@ -16,6 +16,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before constitution update)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_constitution` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Outline. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Outline You are updating the project constitution at `.specify/memory/constitution.md`. This file is a TEMPLATE containing placeholder tokens in square brackets (e.g. `[PROJECT_NAME]`, `[PRINCIPLE_1_NAME]`). Your job is to (a) collect/derive concrete values, (b) fill the template precisely, and (c) propagate any amendments across dependent artifacts. @@ -84,3 +118,35 @@ If the user supplies partial updates (e.g., only one principle revision), still If critical info missing (e.g., ratification date truly unknown), insert `TODO(): explanation` and include in the Sync Impact Report under deferred items. Do not create a new template; always operate on the existing `.specify/memory/constitution.md` file. + +## Post-Execution Checks + +**Check for extension hooks (after constitution update)**: +Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.after_constitution` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/.agents/skills/speckit-git-commit/SKILL.md b/.agents/skills/speckit-git-commit/SKILL.md new file mode 100644 index 0000000..6d1f29a --- /dev/null +++ b/.agents/skills/speckit-git-commit/SKILL.md @@ -0,0 +1,53 @@ +--- +name: speckit-git-commit +description: Auto-commit changes after a Spec Kit command completes +compatibility: Requires spec-kit project structure with .specify/ directory +metadata: + author: github-spec-kit + source: git:commands/speckit.git.commit.md +--- + +# Auto-Commit Changes + +Automatically stage and commit all changes after a Spec Kit command completes. + +## Behavior + +This command is invoked as a hook after (or before) core commands. It: + +1. Determines the event name from the hook context (e.g., if invoked as an `after_specify` hook, the event is `after_specify`; if `before_plan`, the event is `before_plan`) +2. Checks `.specify/extensions/git/git-config.yml` for the `auto_commit` section +3. Looks up the specific event key to see if auto-commit is enabled +4. Falls back to `auto_commit.default` if no event-specific key exists +5. Uses the per-command `message` if configured, otherwise a default message +6. If enabled and there are uncommitted changes, runs `git add .` + `git commit` + +## Execution + +Determine the event name from the hook that triggered this command, then run the script: + +- **Bash**: `.specify/extensions/git/scripts/bash/auto-commit.sh ` +- **PowerShell**: `.specify/extensions/git/scripts/powershell/auto-commit.ps1 ` + +Replace `` with the actual hook event (e.g., `after_specify`, `before_plan`, `after_implement`). + +## Configuration + +In `.specify/extensions/git/git-config.yml`: + +```yaml +auto_commit: + default: false # Global toggle — set true to enable for all commands + after_specify: + enabled: true # Override per-command + message: "[Spec Kit] Add specification" + after_plan: + enabled: false + message: "[Spec Kit] Add implementation plan" +``` + +## Graceful Degradation + +- If Git is not available or the current directory is not a repository: skips with a warning +- If no config file exists: skips (disabled by default) +- If no changes to commit: skips with a message \ No newline at end of file diff --git a/.agents/skills/speckit-git-feature/SKILL.md b/.agents/skills/speckit-git-feature/SKILL.md new file mode 100644 index 0000000..d1c7904 --- /dev/null +++ b/.agents/skills/speckit-git-feature/SKILL.md @@ -0,0 +1,72 @@ +--- +name: speckit-git-feature +description: Create a feature branch with sequential or timestamp numbering +compatibility: Requires spec-kit project structure with .specify/ directory +metadata: + author: github-spec-kit + source: git:commands/speckit.git.feature.md +--- + +# Create Feature Branch + +Create and switch to a new git feature branch for the given specification. This command handles **branch creation only** — the spec directory and files are created by the core `/speckit.specify` workflow. + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Environment Variable Override + +If the user explicitly provided `GIT_BRANCH_NAME` (e.g., via environment variable, argument, or in their request), pass it through to the script by setting the `GIT_BRANCH_NAME` environment variable before invoking the script. When `GIT_BRANCH_NAME` is set: +- The script uses the exact value as the branch name, bypassing all prefix/suffix generation +- `--short-name`, `--number`, and `--timestamp` flags are ignored +- `FEATURE_NUM` is extracted from the name if it starts with a numeric prefix, otherwise set to the full branch name + +## Prerequisites + +- Verify Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null` +- If Git is not available, warn the user and skip branch creation + +## Branch Numbering Mode + +Determine the branch numbering strategy by checking configuration in this order: + +1. Check `.specify/extensions/git/git-config.yml` for `branch_numbering` value +2. Check `.specify/init-options.json` for `branch_numbering` value (backward compatibility) +3. Default to `sequential` if neither exists + +## Execution + +Generate a concise short name (2-4 words) for the branch: +- Analyze the feature description and extract the most meaningful keywords +- Use action-noun format when possible (e.g., "add-user-auth", "fix-payment-bug") +- Preserve technical terms and acronyms (OAuth2, API, JWT, etc.) + +Run the appropriate script based on your platform: + +- **Bash**: `.specify/extensions/git/scripts/bash/create-new-feature.sh --json --short-name "" ""` +- **Bash (timestamp)**: `.specify/extensions/git/scripts/bash/create-new-feature.sh --json --timestamp --short-name "" ""` +- **PowerShell**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -ShortName "" ""` +- **PowerShell (timestamp)**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -Timestamp -ShortName "" ""` + +**IMPORTANT**: +- Do NOT pass `--number` — the script determines the correct next number automatically +- Always include the JSON flag (`--json` for Bash, `-Json` for PowerShell) so the output can be parsed reliably +- You must only ever run this script once per feature +- The JSON output will contain `BRANCH_NAME` and `FEATURE_NUM` + +## Graceful Degradation + +If Git is not installed or the current directory is not a Git repository: +- Branch creation is skipped with a warning: `[specify] Warning: Git repository not detected; skipped branch creation` +- The script still outputs `BRANCH_NAME` and `FEATURE_NUM` so the caller can reference them + +## Output + +The script outputs JSON with: +- `BRANCH_NAME`: The branch name (e.g., `003-user-auth` or `20260319-143022-user-auth`) +- `FEATURE_NUM`: The numeric or timestamp prefix used \ No newline at end of file diff --git a/.agents/skills/speckit-git-initialize/SKILL.md b/.agents/skills/speckit-git-initialize/SKILL.md new file mode 100644 index 0000000..2e31d97 --- /dev/null +++ b/.agents/skills/speckit-git-initialize/SKILL.md @@ -0,0 +1,54 @@ +--- +name: speckit-git-initialize +description: Initialize a Git repository with an initial commit +compatibility: Requires spec-kit project structure with .specify/ directory +metadata: + author: github-spec-kit + source: git:commands/speckit.git.initialize.md +--- + +# Initialize Git Repository + +Initialize a Git repository in the current project directory if one does not already exist. + +## Execution + +Run the appropriate script from the project root: + +- **Bash**: `.specify/extensions/git/scripts/bash/initialize-repo.sh` +- **PowerShell**: `.specify/extensions/git/scripts/powershell/initialize-repo.ps1` + +If the extension scripts are not found, fall back to: +- **Bash**: `git init && git add . && git commit -m "Initial commit from Specify template"` +- **PowerShell**: `git init; git add .; git commit -m "Initial commit from Specify template"` + +The script handles all checks internally: +- Skips if Git is not available +- Skips if already inside a Git repository +- Runs `git init`, `git add .`, and `git commit` with an initial commit message + +## Customization + +Replace the script to add project-specific Git initialization steps: +- Custom `.gitignore` templates +- Default branch naming (`git config init.defaultBranch`) +- Git LFS setup +- Git hooks installation +- Commit signing configuration +- Git Flow initialization + +## Output + +On success: +- `✓ Git repository initialized` + +## Graceful Degradation + +If Git is not installed: +- Warn the user +- Skip repository initialization +- The project continues to function without Git (specs can still be created under `specs/`) + +If Git is installed but `git init`, `git add .`, or `git commit` fails: +- Surface the error to the user +- Stop this command rather than continuing with a partially initialized repository \ No newline at end of file diff --git a/.agents/skills/speckit-git-remote/SKILL.md b/.agents/skills/speckit-git-remote/SKILL.md new file mode 100644 index 0000000..50bfc3d --- /dev/null +++ b/.agents/skills/speckit-git-remote/SKILL.md @@ -0,0 +1,50 @@ +--- +name: speckit-git-remote +description: Detect Git remote URL for GitHub integration +compatibility: Requires spec-kit project structure with .specify/ directory +metadata: + author: github-spec-kit + source: git:commands/speckit.git.remote.md +--- + +# Detect Git Remote URL + +Detect the Git remote URL for integration with GitHub services (e.g., issue creation). + +## Prerequisites + +- Check if Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null` +- If Git is not available, output a warning and return empty: + ``` + [specify] Warning: Git repository not detected; cannot determine remote URL + ``` + +## Execution + +Run the following command to get the remote URL: + +```bash +git config --get remote.origin.url +``` + +## Output + +Parse the remote URL and determine: + +1. **Repository owner**: Extract from the URL (e.g., `github` from `https://github.com/github/spec-kit.git`) +2. **Repository name**: Extract from the URL (e.g., `spec-kit` from `https://github.com/github/spec-kit.git`) +3. **Is GitHub**: Whether the remote points to a GitHub repository + +Supported URL formats: +- HTTPS: `https://github.com//.git` +- SSH: `git@github.com:/.git` + +> [!CAUTION] +> ONLY report a GitHub repository if the remote URL actually points to github.com. +> Do NOT assume the remote is GitHub if the URL format doesn't match. + +## Graceful Degradation + +If Git is not installed, the directory is not a Git repository, or no remote is configured: +- Return an empty result +- Do NOT error — other workflows should continue without Git remote information \ No newline at end of file diff --git a/.agents/skills/speckit-git-validate/SKILL.md b/.agents/skills/speckit-git-validate/SKILL.md new file mode 100644 index 0000000..7655e3a --- /dev/null +++ b/.agents/skills/speckit-git-validate/SKILL.md @@ -0,0 +1,54 @@ +--- +name: speckit-git-validate +description: Validate current branch follows feature branch naming conventions +compatibility: Requires spec-kit project structure with .specify/ directory +metadata: + author: github-spec-kit + source: git:commands/speckit.git.validate.md +--- + +# Validate Feature Branch + +Validate that the current Git branch follows the expected feature branch naming conventions. + +## Prerequisites + +- Check if Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null` +- If Git is not available, output a warning and skip validation: + ``` + [specify] Warning: Git repository not detected; skipped branch validation + ``` + +## Validation Rules + +Get the current branch name: + +```bash +git rev-parse --abbrev-ref HEAD +``` + +The branch name must match one of these patterns: + +1. **Sequential**: `^[0-9]{3,}-` (e.g., `001-feature-name`, `042-fix-bug`, `1000-big-feature`) +2. **Timestamp**: `^[0-9]{8}-[0-9]{6}-` (e.g., `20260319-143022-feature-name`) + +## Execution + +If on a feature branch (matches either pattern): +- Output: `✓ On feature branch: ` +- Check if the corresponding spec directory exists under `specs/`: + - For sequential branches, look for `specs/-*` where prefix matches the numeric portion + - For timestamp branches, look for `specs/-*` where prefix matches the `YYYYMMDD-HHMMSS` portion +- If spec directory exists: `✓ Spec directory found: ` +- If spec directory missing: `⚠ No spec directory found for prefix ` + +If NOT on a feature branch: +- Output: `✗ Not on a feature branch. Current branch: ` +- Output: `Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name` + +## Graceful Degradation + +If Git is not installed or the directory is not a Git repository: +- Check the `SPECIFY_FEATURE` environment variable as a fallback +- If set, validate that value against the naming patterns +- If not set, skip validation with a warning \ No newline at end of file diff --git a/.agents/skills/speckit-plan/SKILL.md b/.agents/skills/speckit-plan/SKILL.md index 5b3cddb..c4be849 100644 --- a/.agents/skills/speckit-plan/SKILL.md +++ b/.agents/skills/speckit-plan/SKILL.md @@ -137,15 +137,11 @@ You **MUST** consider the user input before proceeding (if not empty). - Skip if project is purely internal (build scripts, one-off tools, etc.) 3. **Agent context update**: - - Run `.specify/scripts/bash/update-agent-context.sh codex` - - These scripts detect which AI agent is in use - - Update the appropriate agent-specific context file - - Add only new technology from current plan - - Preserve manual additions between markers + - Update the plan reference between the `` and `` markers in `AGENTS.md` to point to the plan file created in step 1 (the IMPL_PLAN path) -**Output**: data-model.md, /contracts/*, quickstart.md, agent-specific file +**Output**: data-model.md, /contracts/*, quickstart.md, updated agent context file ## Key rules -- Use absolute paths +- Use absolute paths for filesystem operations; use project-relative paths for references in documentation and agent context files - ERROR on gate failures or unresolved clarifications diff --git a/.agents/skills/speckit-specify/SKILL.md b/.agents/skills/speckit-specify/SKILL.md index c21a22b..87197d5 100644 --- a/.agents/skills/speckit-specify/SKILL.md +++ b/.agents/skills/speckit-specify/SKILL.md @@ -56,7 +56,7 @@ The text the user typed after `/speckit.specify` in the triggering message **is* Given that feature description, do this: -1. **Generate a concise short name** (2-4 words) for the branch: +1. **Generate a concise short name** (2-4 words) for the feature: - Analyze the feature description and extract the most meaningful keywords - Create a 2-4 word short name that captures the essence of the feature - Use action-noun format when possible (e.g., "add-user-auth", "fix-payment-bug") @@ -68,30 +68,47 @@ Given that feature description, do this: - "Create a dashboard for analytics" → "analytics-dashboard" - "Fix payment processing timeout bug" → "fix-payment-timeout" -2. **Create the feature branch** by running the script with `--short-name` (and `--json`). In sequential mode, do NOT pass `--number` — the script auto-detects the next available number. In timestamp mode, the script generates a `YYYYMMDD-HHMMSS` prefix automatically: +2. **Branch creation** (optional, via hook): - **Branch numbering mode**: Before running the script, check if `.specify/init-options.json` exists and read the `branch_numbering` value. - - If `"timestamp"`, add `--timestamp` (Bash) or `-Timestamp` (PowerShell) to the script invocation - - If `"sequential"` or absent, do not add any extra flag (default behavior) + If a `before_specify` hook ran successfully in the Pre-Execution Checks above, it will have created/switched to a git branch and output JSON containing `BRANCH_NAME` and `FEATURE_NUM`. Note these values for reference, but the branch name does **not** dictate the spec directory name. - - Bash example: `.specify/scripts/bash/create-new-feature.sh "$ARGUMENTS" --json --short-name "user-auth" "Add user authentication"` - - Bash (timestamp): `.specify/scripts/bash/create-new-feature.sh "$ARGUMENTS" --json --timestamp --short-name "user-auth" "Add user authentication"` - - PowerShell example: `.specify/scripts/bash/create-new-feature.sh "$ARGUMENTS" -Json -ShortName "user-auth" "Add user authentication"` - - PowerShell (timestamp): `.specify/scripts/bash/create-new-feature.sh "$ARGUMENTS" -Json -Timestamp -ShortName "user-auth" "Add user authentication"` + If the user explicitly provided `GIT_BRANCH_NAME`, pass it through to the hook so the branch script uses the exact value as the branch name (bypassing all prefix/suffix generation). - **IMPORTANT**: - - Do NOT pass `--number` — the script determines the correct next number automatically - - Always include the JSON flag (`--json` for Bash, `-Json` for PowerShell) so the output can be parsed reliably - - You must only ever run this script once per feature - - The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for - - The JSON output will contain BRANCH_NAME and SPEC_FILE paths - - For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot") +3. **Create the spec feature directory**: + + Specs live under the default `specs/` directory unless the user explicitly provides `SPECIFY_FEATURE_DIRECTORY`. + + **Resolution order for `SPECIFY_FEATURE_DIRECTORY`**: + 1. If the user explicitly provided `SPECIFY_FEATURE_DIRECTORY` (e.g., via environment variable, argument, or configuration), use it as-is + 2. Otherwise, auto-generate it under `specs/`: + - Check `.specify/init-options.json` for `branch_numbering` + - If `"timestamp"`: prefix is `YYYYMMDD-HHMMSS` (current timestamp) + - If `"sequential"` or absent: prefix is `NNN` (next available 3-digit number after scanning existing directories in `specs/`) + - Construct the directory name: `-` (e.g., `003-user-auth` or `20260319-143022-user-auth`) + - Set `SPECIFY_FEATURE_DIRECTORY` to `specs/` -3. Load `.specify/templates/spec-template.md` to understand required sections. + **Create the directory and spec file**: + - `mkdir -p SPECIFY_FEATURE_DIRECTORY` + - Copy `.specify/templates/spec-template.md` to `SPECIFY_FEATURE_DIRECTORY/spec.md` as the starting point + - Set `SPEC_FILE` to `SPECIFY_FEATURE_DIRECTORY/spec.md` + - Persist the resolved path to `.specify/feature.json`: + ```json + { + "feature_directory": "" + } + ``` + Write the actual resolved directory path value (for example, `specs/003-user-auth`), not the literal string `SPECIFY_FEATURE_DIRECTORY`. + This allows downstream commands (`/speckit.plan`, `/speckit.tasks`, etc.) to locate the feature directory without relying on git branch name conventions. + + **IMPORTANT**: + - You must only create one feature per `/speckit.specify` invocation + - The spec directory name and the git branch name are independent — they may be the same but that is the user's choice + - The spec directory and file are always created by this command, never by the hook -4. Follow this execution flow: +4. Load `.specify/templates/spec-template.md` to understand required sections. - 1. Parse user description from Input +5. Follow this execution flow: + 1. Parse user description from arguments If empty: ERROR "No feature description provided" 2. Extract key concepts from description Identify: actors, actions, data, constraints @@ -115,11 +132,11 @@ Given that feature description, do this: 7. Identify Key Entities (if data involved) 8. Return: SUCCESS (spec ready for planning) -5. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings. +6. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings. -6. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria: +7. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria: - a. **Create Spec Quality Checklist**: Generate a checklist file at `FEATURE_DIR/checklists/requirements.md` using the checklist template structure with these validation items: + a. **Create Spec Quality Checklist**: Generate a checklist file at `SPECIFY_FEATURE_DIRECTORY/checklists/requirements.md` using the checklist template structure with these validation items: ```markdown # Specification Quality Checklist: [FEATURE NAME] @@ -209,9 +226,13 @@ Given that feature description, do this: d. **Update Checklist**: After each validation iteration, update the checklist file with current pass/fail status -7. Report completion with branch name, spec file path, checklist results, and readiness for the next phase (`/speckit.clarify` or `/speckit.plan`). +8. **Report completion** to the user with: + - `SPECIFY_FEATURE_DIRECTORY` — the feature directory path + - `SPEC_FILE` — the spec file path + - Checklist results summary + - Readiness for the next phase (`/speckit.clarify` or `/speckit.plan`) -8. **Check for extension hooks**: After reporting completion, check if `.specify/extensions.yml` exists in the project root. +9. **Check for extension hooks**: After reporting completion, check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.after_specify` key - If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. @@ -240,7 +261,7 @@ Given that feature description, do this: ``` - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently -**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing. +**NOTE:** Branch creation is handled by the `before_specify` hook (git extension). Spec directory and file creation are always handled by this core command. ## Quick Guidelines diff --git a/.agents/skills/speckit-taskstoissues/SKILL.md b/.agents/skills/speckit-taskstoissues/SKILL.md index 48158b7..7d303b1 100644 --- a/.agents/skills/speckit-taskstoissues/SKILL.md +++ b/.agents/skills/speckit-taskstoissues/SKILL.md @@ -16,6 +16,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before tasks-to-issues conversion)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_taskstoissues` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Outline. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Outline 1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). @@ -33,3 +67,35 @@ git config --get remote.origin.url > [!CAUTION] > UNDER NO CIRCUMSTANCES EVER CREATE ISSUES IN REPOSITORIES THAT DO NOT MATCH THE REMOTE URL + +## Post-Execution Checks + +**Check for extension hooks (after tasks-to-issues conversion)**: +Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.after_taskstoissues` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/.claude/skills/speckit-analyze/SKILL.md b/.claude/skills/speckit-analyze/SKILL.md new file mode 100644 index 0000000..62eaf90 --- /dev/null +++ b/.claude/skills/speckit-analyze/SKILL.md @@ -0,0 +1,260 @@ +--- +name: "speckit-analyze" +description: "Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation." +argument-hint: "Optional focus areas for analysis" +compatibility: "Requires spec-kit project structure with .specify/ directory" +metadata: + author: "github-spec-kit" + source: "templates/commands/analyze.md" +user-invocable: true +disable-model-invocation: false +--- + + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Pre-Execution Checks + +**Check for extension hooks (before analysis)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_analyze` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Goal. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + +## Goal + +Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit.tasks` has successfully produced a complete `tasks.md`. + +## Operating Constraints + +**STRICTLY READ-ONLY**: Do **not** modify any files. Output a structured analysis report. Offer an optional remediation plan (user must explicitly approve before any follow-up editing commands would be invoked manually). + +**Constitution Authority**: The project constitution (`.specify/memory/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks—not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `/speckit.analyze`. + +## Execution Steps + +### 1. Initialize Analysis Context + +Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS. Derive absolute paths: + +- SPEC = FEATURE_DIR/spec.md +- PLAN = FEATURE_DIR/plan.md +- TASKS = FEATURE_DIR/tasks.md + +Abort with an error message if any required file is missing (instruct the user to run missing prerequisite command). +For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). + +### 2. Load Artifacts (Progressive Disclosure) + +Load only the minimal necessary context from each artifact: + +**From spec.md:** + +- Overview/Context +- Functional Requirements +- Success Criteria (measurable outcomes — e.g., performance, security, availability, user success, business impact) +- User Stories +- Edge Cases (if present) + +**From plan.md:** + +- Architecture/stack choices +- Data Model references +- Phases +- Technical constraints + +**From tasks.md:** + +- Task IDs +- Descriptions +- Phase grouping +- Parallel markers [P] +- Referenced file paths + +**From constitution:** + +- Load `.specify/memory/constitution.md` for principle validation + +### 3. Build Semantic Models + +Create internal representations (do not include raw artifacts in output): + +- **Requirements inventory**: For each Functional Requirement (FR-###) and Success Criterion (SC-###), record a stable key. Use the explicit FR-/SC- identifier as the primary key when present, and optionally also derive an imperative-phrase slug for readability (e.g., "User can upload file" → `user-can-upload-file`). Include only Success Criteria items that require buildable work (e.g., load-testing infrastructure, security audit tooling), and exclude post-launch outcome metrics and business KPIs (e.g., "Reduce support tickets by 50%"). +- **User story/action inventory**: Discrete user actions with acceptance criteria +- **Task coverage mapping**: Map each task to one or more requirements or stories (inference by keyword / explicit reference patterns like IDs or key phrases) +- **Constitution rule set**: Extract principle names and MUST/SHOULD normative statements + +### 4. Detection Passes (Token-Efficient Analysis) + +Focus on high-signal findings. Limit to 50 findings total; aggregate remainder in overflow summary. + +#### A. Duplication Detection + +- Identify near-duplicate requirements +- Mark lower-quality phrasing for consolidation + +#### B. Ambiguity Detection + +- Flag vague adjectives (fast, scalable, secure, intuitive, robust) lacking measurable criteria +- Flag unresolved placeholders (TODO, TKTK, ???, ``, etc.) + +#### C. Underspecification + +- Requirements with verbs but missing object or measurable outcome +- User stories missing acceptance criteria alignment +- Tasks referencing files or components not defined in spec/plan + +#### D. Constitution Alignment + +- Any requirement or plan element conflicting with a MUST principle +- Missing mandated sections or quality gates from constitution + +#### E. Coverage Gaps + +- Requirements with zero associated tasks +- Tasks with no mapped requirement/story +- Success Criteria requiring buildable work (performance, security, availability) not reflected in tasks + +#### F. Inconsistency + +- Terminology drift (same concept named differently across files) +- Data entities referenced in plan but absent in spec (or vice versa) +- Task ordering contradictions (e.g., integration tasks before foundational setup tasks without dependency note) +- Conflicting requirements (e.g., one requires Next.js while other specifies Vue) + +### 5. Severity Assignment + +Use this heuristic to prioritize findings: + +- **CRITICAL**: Violates constitution MUST, missing core spec artifact, or requirement with zero coverage that blocks baseline functionality +- **HIGH**: Duplicate or conflicting requirement, ambiguous security/performance attribute, untestable acceptance criterion +- **MEDIUM**: Terminology drift, missing non-functional task coverage, underspecified edge case +- **LOW**: Style/wording improvements, minor redundancy not affecting execution order + +### 6. Produce Compact Analysis Report + +Output a Markdown report (no file writes) with the following structure: + +## Specification Analysis Report + +| ID | Category | Severity | Location(s) | Summary | Recommendation | +|----|----------|----------|-------------|---------|----------------| +| A1 | Duplication | HIGH | spec.md:L120-134 | Two similar requirements ... | Merge phrasing; keep clearer version | + +(Add one row per finding; generate stable IDs prefixed by category initial.) + +**Coverage Summary Table:** + +| Requirement Key | Has Task? | Task IDs | Notes | +|-----------------|-----------|----------|-------| + +**Constitution Alignment Issues:** (if any) + +**Unmapped Tasks:** (if any) + +**Metrics:** + +- Total Requirements +- Total Tasks +- Coverage % (requirements with >=1 task) +- Ambiguity Count +- Duplication Count +- Critical Issues Count + +### 7. Provide Next Actions + +At end of report, output a concise Next Actions block: + +- If CRITICAL issues exist: Recommend resolving before `/speckit.implement` +- If only LOW/MEDIUM: User may proceed, but provide improvement suggestions +- Provide explicit command suggestions: e.g., "Run /speckit.specify with refinement", "Run /speckit.plan to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'" + +### 8. Offer Remediation + +Ask the user: "Would you like me to suggest concrete remediation edits for the top N issues?" (Do NOT apply them automatically.) + +### 9. Check for extension hooks + +After reporting, check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.after_analyze` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + +## Operating Principles + +### Context Efficiency + +- **Minimal high-signal tokens**: Focus on actionable findings, not exhaustive documentation +- **Progressive disclosure**: Load artifacts incrementally; don't dump all content into analysis +- **Token-efficient output**: Limit findings table to 50 rows; summarize overflow +- **Deterministic results**: Rerunning without changes should produce consistent IDs and counts + +### Analysis Guidelines + +- **NEVER modify files** (this is read-only analysis) +- **NEVER hallucinate missing sections** (if absent, report them accurately) +- **Prioritize constitution violations** (these are always CRITICAL) +- **Use examples over exhaustive rules** (cite specific instances, not generic patterns) +- **Report zero issues gracefully** (emit success report with coverage statistics) + +## Context + +$ARGUMENTS diff --git a/.claude/skills/speckit-checklist/SKILL.md b/.claude/skills/speckit-checklist/SKILL.md new file mode 100644 index 0000000..c61ee6e --- /dev/null +++ b/.claude/skills/speckit-checklist/SKILL.md @@ -0,0 +1,372 @@ +--- +name: "speckit-checklist" +description: "Generate a custom checklist for the current feature based on user requirements." +argument-hint: "Domain or focus area for the checklist" +compatibility: "Requires spec-kit project structure with .specify/ directory" +metadata: + author: "github-spec-kit" + source: "templates/commands/checklist.md" +user-invocable: true +disable-model-invocation: false +--- + + +## Checklist Purpose: "Unit Tests for English" + +**CRITICAL CONCEPT**: Checklists are **UNIT TESTS FOR REQUIREMENTS WRITING** - they validate the quality, clarity, and completeness of requirements in a given domain. + +**NOT for verification/testing**: + +- ❌ NOT "Verify the button clicks correctly" +- ❌ NOT "Test error handling works" +- ❌ NOT "Confirm the API returns 200" +- ❌ NOT checking if code/implementation matches the spec + +**FOR requirements quality validation**: + +- ✅ "Are visual hierarchy requirements defined for all card types?" (completeness) +- ✅ "Is 'prominent display' quantified with specific sizing/positioning?" (clarity) +- ✅ "Are hover state requirements consistent across all interactive elements?" (consistency) +- ✅ "Are accessibility requirements defined for keyboard navigation?" (coverage) +- ✅ "Does the spec define what happens when logo image fails to load?" (edge cases) + +**Metaphor**: If your spec is code written in English, the checklist is its unit test suite. You're testing whether the requirements are well-written, complete, unambiguous, and ready for implementation - NOT whether the implementation works. + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Pre-Execution Checks + +**Check for extension hooks (before checklist generation)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_checklist` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Execution Steps. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + +## Execution Steps + +1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS list. + - All file paths must be absolute. + - For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). + +2. **Clarify intent (dynamic)**: Derive up to THREE initial contextual clarifying questions (no pre-baked catalog). They MUST: + - Be generated from the user's phrasing + extracted signals from spec/plan/tasks + - Only ask about information that materially changes checklist content + - Be skipped individually if already unambiguous in `$ARGUMENTS` + - Prefer precision over breadth + + Generation algorithm: + 1. Extract signals: feature domain keywords (e.g., auth, latency, UX, API), risk indicators ("critical", "must", "compliance"), stakeholder hints ("QA", "review", "security team"), and explicit deliverables ("a11y", "rollback", "contracts"). + 2. Cluster signals into candidate focus areas (max 4) ranked by relevance. + 3. Identify probable audience & timing (author, reviewer, QA, release) if not explicit. + 4. Detect missing dimensions: scope breadth, depth/rigor, risk emphasis, exclusion boundaries, measurable acceptance criteria. + 5. Formulate questions chosen from these archetypes: + - Scope refinement (e.g., "Should this include integration touchpoints with X and Y or stay limited to local module correctness?") + - Risk prioritization (e.g., "Which of these potential risk areas should receive mandatory gating checks?") + - Depth calibration (e.g., "Is this a lightweight pre-commit sanity list or a formal release gate?") + - Audience framing (e.g., "Will this be used by the author only or peers during PR review?") + - Boundary exclusion (e.g., "Should we explicitly exclude performance tuning items this round?") + - Scenario class gap (e.g., "No recovery flows detected—are rollback / partial failure paths in scope?") + + Question formatting rules: + - If presenting options, generate a compact table with columns: Option | Candidate | Why It Matters + - Limit to A–E options maximum; omit table if a free-form answer is clearer + - Never ask the user to restate what they already said + - Avoid speculative categories (no hallucination). If uncertain, ask explicitly: "Confirm whether X belongs in scope." + + Defaults when interaction impossible: + - Depth: Standard + - Audience: Reviewer (PR) if code-related; Author otherwise + - Focus: Top 2 relevance clusters + + Output the questions (label Q1/Q2/Q3). After answers: if ≥2 scenario classes (Alternate / Exception / Recovery / Non-Functional domain) remain unclear, you MAY ask up to TWO more targeted follow‑ups (Q4/Q5) with a one-line justification each (e.g., "Unresolved recovery path risk"). Do not exceed five total questions. Skip escalation if user explicitly declines more. + +3. **Understand user request**: Combine `$ARGUMENTS` + clarifying answers: + - Derive checklist theme (e.g., security, review, deploy, ux) + - Consolidate explicit must-have items mentioned by user + - Map focus selections to category scaffolding + - Infer any missing context from spec/plan/tasks (do NOT hallucinate) + +4. **Load feature context**: Read from FEATURE_DIR: + - spec.md: Feature requirements and scope + - plan.md (if exists): Technical details, dependencies + - tasks.md (if exists): Implementation tasks + + **Context Loading Strategy**: + - Load only necessary portions relevant to active focus areas (avoid full-file dumping) + - Prefer summarizing long sections into concise scenario/requirement bullets + - Use progressive disclosure: add follow-on retrieval only if gaps detected + - If source docs are large, generate interim summary items instead of embedding raw text + +5. **Generate checklist** - Create "Unit Tests for Requirements": + - Create `FEATURE_DIR/checklists/` directory if it doesn't exist + - Generate unique checklist filename: + - Use short, descriptive name based on domain (e.g., `ux.md`, `api.md`, `security.md`) + - Format: `[domain].md` + - File handling behavior: + - If file does NOT exist: Create new file and number items starting from CHK001 + - If file exists: Append new items to existing file, continuing from the last CHK ID (e.g., if last item is CHK015, start new items at CHK016) + - Never delete or replace existing checklist content - always preserve and append + + **CORE PRINCIPLE - Test the Requirements, Not the Implementation**: + Every checklist item MUST evaluate the REQUIREMENTS THEMSELVES for: + - **Completeness**: Are all necessary requirements present? + - **Clarity**: Are requirements unambiguous and specific? + - **Consistency**: Do requirements align with each other? + - **Measurability**: Can requirements be objectively verified? + - **Coverage**: Are all scenarios/edge cases addressed? + + **Category Structure** - Group items by requirement quality dimensions: + - **Requirement Completeness** (Are all necessary requirements documented?) + - **Requirement Clarity** (Are requirements specific and unambiguous?) + - **Requirement Consistency** (Do requirements align without conflicts?) + - **Acceptance Criteria Quality** (Are success criteria measurable?) + - **Scenario Coverage** (Are all flows/cases addressed?) + - **Edge Case Coverage** (Are boundary conditions defined?) + - **Non-Functional Requirements** (Performance, Security, Accessibility, etc. - are they specified?) + - **Dependencies & Assumptions** (Are they documented and validated?) + - **Ambiguities & Conflicts** (What needs clarification?) + + **HOW TO WRITE CHECKLIST ITEMS - "Unit Tests for English"**: + + ❌ **WRONG** (Testing implementation): + - "Verify landing page displays 3 episode cards" + - "Test hover states work on desktop" + - "Confirm logo click navigates home" + + ✅ **CORRECT** (Testing requirements quality): + - "Are the exact number and layout of featured episodes specified?" [Completeness] + - "Is 'prominent display' quantified with specific sizing/positioning?" [Clarity] + - "Are hover state requirements consistent across all interactive elements?" [Consistency] + - "Are keyboard navigation requirements defined for all interactive UI?" [Coverage] + - "Is the fallback behavior specified when logo image fails to load?" [Edge Cases] + - "Are loading states defined for asynchronous episode data?" [Completeness] + - "Does the spec define visual hierarchy for competing UI elements?" [Clarity] + + **ITEM STRUCTURE**: + Each item should follow this pattern: + - Question format asking about requirement quality + - Focus on what's WRITTEN (or not written) in the spec/plan + - Include quality dimension in brackets [Completeness/Clarity/Consistency/etc.] + - Reference spec section `[Spec §X.Y]` when checking existing requirements + - Use `[Gap]` marker when checking for missing requirements + + **EXAMPLES BY QUALITY DIMENSION**: + + Completeness: + - "Are error handling requirements defined for all API failure modes? [Gap]" + - "Are accessibility requirements specified for all interactive elements? [Completeness]" + - "Are mobile breakpoint requirements defined for responsive layouts? [Gap]" + + Clarity: + - "Is 'fast loading' quantified with specific timing thresholds? [Clarity, Spec §NFR-2]" + - "Are 'related episodes' selection criteria explicitly defined? [Clarity, Spec §FR-5]" + - "Is 'prominent' defined with measurable visual properties? [Ambiguity, Spec §FR-4]" + + Consistency: + - "Do navigation requirements align across all pages? [Consistency, Spec §FR-10]" + - "Are card component requirements consistent between landing and detail pages? [Consistency]" + + Coverage: + - "Are requirements defined for zero-state scenarios (no episodes)? [Coverage, Edge Case]" + - "Are concurrent user interaction scenarios addressed? [Coverage, Gap]" + - "Are requirements specified for partial data loading failures? [Coverage, Exception Flow]" + + Measurability: + - "Are visual hierarchy requirements measurable/testable? [Acceptance Criteria, Spec §FR-1]" + - "Can 'balanced visual weight' be objectively verified? [Measurability, Spec §FR-2]" + + **Scenario Classification & Coverage** (Requirements Quality Focus): + - Check if requirements exist for: Primary, Alternate, Exception/Error, Recovery, Non-Functional scenarios + - For each scenario class, ask: "Are [scenario type] requirements complete, clear, and consistent?" + - If scenario class missing: "Are [scenario type] requirements intentionally excluded or missing? [Gap]" + - Include resilience/rollback when state mutation occurs: "Are rollback requirements defined for migration failures? [Gap]" + + **Traceability Requirements**: + - MINIMUM: ≥80% of items MUST include at least one traceability reference + - Each item should reference: spec section `[Spec §X.Y]`, or use markers: `[Gap]`, `[Ambiguity]`, `[Conflict]`, `[Assumption]` + - If no ID system exists: "Is a requirement & acceptance criteria ID scheme established? [Traceability]" + + **Surface & Resolve Issues** (Requirements Quality Problems): + Ask questions about the requirements themselves: + - Ambiguities: "Is the term 'fast' quantified with specific metrics? [Ambiguity, Spec §NFR-1]" + - Conflicts: "Do navigation requirements conflict between §FR-10 and §FR-10a? [Conflict]" + - Assumptions: "Is the assumption of 'always available podcast API' validated? [Assumption]" + - Dependencies: "Are external podcast API requirements documented? [Dependency, Gap]" + - Missing definitions: "Is 'visual hierarchy' defined with measurable criteria? [Gap]" + + **Content Consolidation**: + - Soft cap: If raw candidate items > 40, prioritize by risk/impact + - Merge near-duplicates checking the same requirement aspect + - If >5 low-impact edge cases, create one item: "Are edge cases X, Y, Z addressed in requirements? [Coverage]" + + **🚫 ABSOLUTELY PROHIBITED** - These make it an implementation test, not a requirements test: + - ❌ Any item starting with "Verify", "Test", "Confirm", "Check" + implementation behavior + - ❌ References to code execution, user actions, system behavior + - ❌ "Displays correctly", "works properly", "functions as expected" + - ❌ "Click", "navigate", "render", "load", "execute" + - ❌ Test cases, test plans, QA procedures + - ❌ Implementation details (frameworks, APIs, algorithms) + + **✅ REQUIRED PATTERNS** - These test requirements quality: + - ✅ "Are [requirement type] defined/specified/documented for [scenario]?" + - ✅ "Is [vague term] quantified/clarified with specific criteria?" + - ✅ "Are requirements consistent between [section A] and [section B]?" + - ✅ "Can [requirement] be objectively measured/verified?" + - ✅ "Are [edge cases/scenarios] addressed in requirements?" + - ✅ "Does the spec define [missing aspect]?" + +6. **Structure Reference**: Generate the checklist following the canonical template in `.specify/templates/checklist-template.md` for title, meta section, category headings, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, `##` category sections containing `- [ ] CHK### ` lines with globally incrementing IDs starting at CHK001. + +7. **Report**: Output full path to checklist file, item count, and summarize whether the run created a new file or appended to an existing one. Summarize: + - Focus areas selected + - Depth level + - Actor/timing + - Any explicit user-specified must-have items incorporated + +**Important**: Each `/speckit.checklist` command invocation uses a short, descriptive checklist filename and either creates a new file or appends to an existing one. This allows: + +- Multiple checklists of different types (e.g., `ux.md`, `test.md`, `security.md`) +- Simple, memorable filenames that indicate checklist purpose +- Easy identification and navigation in the `checklists/` folder + +To avoid clutter, use descriptive types and clean up obsolete checklists when done. + +## Example Checklist Types & Sample Items + +**UX Requirements Quality:** `ux.md` + +Sample items (testing the requirements, NOT the implementation): + +- "Are visual hierarchy requirements defined with measurable criteria? [Clarity, Spec §FR-1]" +- "Is the number and positioning of UI elements explicitly specified? [Completeness, Spec §FR-1]" +- "Are interaction state requirements (hover, focus, active) consistently defined? [Consistency]" +- "Are accessibility requirements specified for all interactive elements? [Coverage, Gap]" +- "Is fallback behavior defined when images fail to load? [Edge Case, Gap]" +- "Can 'prominent display' be objectively measured? [Measurability, Spec §FR-4]" + +**API Requirements Quality:** `api.md` + +Sample items: + +- "Are error response formats specified for all failure scenarios? [Completeness]" +- "Are rate limiting requirements quantified with specific thresholds? [Clarity]" +- "Are authentication requirements consistent across all endpoints? [Consistency]" +- "Are retry/timeout requirements defined for external dependencies? [Coverage, Gap]" +- "Is versioning strategy documented in requirements? [Gap]" + +**Performance Requirements Quality:** `performance.md` + +Sample items: + +- "Are performance requirements quantified with specific metrics? [Clarity]" +- "Are performance targets defined for all critical user journeys? [Coverage]" +- "Are performance requirements under different load conditions specified? [Completeness]" +- "Can performance requirements be objectively measured? [Measurability]" +- "Are degradation requirements defined for high-load scenarios? [Edge Case, Gap]" + +**Security Requirements Quality:** `security.md` + +Sample items: + +- "Are authentication requirements specified for all protected resources? [Coverage]" +- "Are data protection requirements defined for sensitive information? [Completeness]" +- "Is the threat model documented and requirements aligned to it? [Traceability]" +- "Are security requirements consistent with compliance obligations? [Consistency]" +- "Are security failure/breach response requirements defined? [Gap, Exception Flow]" + +## Anti-Examples: What NOT To Do + +**❌ WRONG - These test implementation, not requirements:** + +```markdown +- [ ] CHK001 - Verify landing page displays 3 episode cards [Spec §FR-001] +- [ ] CHK002 - Test hover states work correctly on desktop [Spec §FR-003] +- [ ] CHK003 - Confirm logo click navigates to home page [Spec §FR-010] +- [ ] CHK004 - Check that related episodes section shows 3-5 items [Spec §FR-005] +``` + +**✅ CORRECT - These test requirements quality:** + +```markdown +- [ ] CHK001 - Are the number and layout of featured episodes explicitly specified? [Completeness, Spec §FR-001] +- [ ] CHK002 - Are hover state requirements consistently defined for all interactive elements? [Consistency, Spec §FR-003] +- [ ] CHK003 - Are navigation requirements clear for all clickable brand elements? [Clarity, Spec §FR-010] +- [ ] CHK004 - Is the selection criteria for related episodes documented? [Gap, Spec §FR-005] +- [ ] CHK005 - Are loading state requirements defined for asynchronous episode data? [Gap] +- [ ] CHK006 - Can "visual hierarchy" requirements be objectively measured? [Measurability, Spec §FR-001] +``` + +**Key Differences:** + +- Wrong: Tests if the system works correctly +- Correct: Tests if the requirements are written correctly +- Wrong: Verification of behavior +- Correct: Validation of requirement quality +- Wrong: "Does it do X?" +- Correct: "Is X clearly specified?" + +## Post-Execution Checks + +**Check for extension hooks (after checklist generation)**: +Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.after_checklist` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/.claude/skills/speckit-clarify/SKILL.md b/.claude/skills/speckit-clarify/SKILL.md new file mode 100644 index 0000000..83cc814 --- /dev/null +++ b/.claude/skills/speckit-clarify/SKILL.md @@ -0,0 +1,254 @@ +--- +name: "speckit-clarify" +description: "Identify underspecified areas in the current feature spec by asking up to 5 highly targeted clarification questions and encoding answers back into the spec." +argument-hint: "Optional areas to clarify in the spec" +compatibility: "Requires spec-kit project structure with .specify/ directory" +metadata: + author: "github-spec-kit" + source: "templates/commands/clarify.md" +user-invocable: true +disable-model-invocation: false +--- + + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Pre-Execution Checks + +**Check for extension hooks (before clarification)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_clarify` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Outline. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + +## Outline + +Goal: Detect and reduce ambiguity or missing decision points in the active feature specification and record the clarifications directly in the spec file. + +Note: This clarification workflow is expected to run (and be completed) BEFORE invoking `/speckit.plan`. If the user explicitly states they are skipping clarification (e.g., exploratory spike), you may proceed, but must warn that downstream rework risk increases. + +Execution steps: + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from repo root **once** (combined `--json --paths-only` mode / `-Json -PathsOnly`). Parse minimal JSON payload fields: + - `FEATURE_DIR` + - `FEATURE_SPEC` + - (Optionally capture `IMPL_PLAN`, `TASKS` for future chained flows.) + - If JSON parsing fails, abort and instruct user to re-run `/speckit.specify` or verify feature branch environment. + - For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). + +2. Load the current spec file. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked). + + Functional Scope & Behavior: + - Core user goals & success criteria + - Explicit out-of-scope declarations + - User roles / personas differentiation + + Domain & Data Model: + - Entities, attributes, relationships + - Identity & uniqueness rules + - Lifecycle/state transitions + - Data volume / scale assumptions + + Interaction & UX Flow: + - Critical user journeys / sequences + - Error/empty/loading states + - Accessibility or localization notes + + Non-Functional Quality Attributes: + - Performance (latency, throughput targets) + - Scalability (horizontal/vertical, limits) + - Reliability & availability (uptime, recovery expectations) + - Observability (logging, metrics, tracing signals) + - Security & privacy (authN/Z, data protection, threat assumptions) + - Compliance / regulatory constraints (if any) + + Integration & External Dependencies: + - External services/APIs and failure modes + - Data import/export formats + - Protocol/versioning assumptions + + Edge Cases & Failure Handling: + - Negative scenarios + - Rate limiting / throttling + - Conflict resolution (e.g., concurrent edits) + + Constraints & Tradeoffs: + - Technical constraints (language, storage, hosting) + - Explicit tradeoffs or rejected alternatives + + Terminology & Consistency: + - Canonical glossary terms + - Avoided synonyms / deprecated terms + + Completion Signals: + - Acceptance criteria testability + - Measurable Definition of Done style indicators + + Misc / Placeholders: + - TODO markers / unresolved decisions + - Ambiguous adjectives ("robust", "intuitive") lacking quantification + + For each category with Partial or Missing status, add a candidate question opportunity unless: + - Clarification would not materially change implementation or validation strategy + - Information is better deferred to planning phase (note internally) + +3. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints: + - Maximum of 5 total questions across the whole session. + - Each question must be answerable with EITHER: + - A short multiple‑choice selection (2–5 distinct, mutually exclusive options), OR + - A one-word / short‑phrase answer (explicitly constrain: "Answer in <=5 words"). + - Only include questions whose answers materially impact architecture, data modeling, task decomposition, test design, UX behavior, operational readiness, or compliance validation. + - Ensure category coverage balance: attempt to cover the highest impact unresolved categories first; avoid asking two low-impact questions when a single high-impact area (e.g., security posture) is unresolved. + - Exclude questions already answered, trivial stylistic preferences, or plan-level execution details (unless blocking correctness). + - Favor clarifications that reduce downstream rework risk or prevent misaligned acceptance tests. + - If more than 5 categories remain unresolved, select the top 5 by (Impact * Uncertainty) heuristic. + +4. Sequential questioning loop (interactive): + - Present EXACTLY ONE question at a time. + - For multiple‑choice questions: + - **Analyze all options** and determine the **most suitable option** based on: + - Best practices for the project type + - Common patterns in similar implementations + - Risk reduction (security, performance, maintainability) + - Alignment with any explicit project goals or constraints visible in the spec + - Present your **recommended option prominently** at the top with clear reasoning (1-2 sentences explaining why this is the best choice). + - Format as: `**Recommended:** Option [X] - ` + - Then render all options as a Markdown table: + + | Option | Description | + |--------|-------------| + | A |