629 lines
41 KiB
Markdown
629 lines
41 KiB
Markdown
# PLAN — Graf szpitali (wielowojewódzki), architektura warstwowa Java 21
|
||
|
||
**Zakres i cel**
|
||
- Zbudować graf w Neo4j łączący szpitale w **całej Polsce** (wszystkie 16 województw, nie tylko podlaskie) z ich kadrą kierowniczą, organami nadzorczymi (rada społeczna / rada nadzorcza), partiami/komitetami wyborczymi oraz mandatami samorządowymi (radni sejmiku, radni powiatu, wójtowie/burmistrzowie, posłowie, senatorowie).
|
||
- Każdy węzeł osoby i szpitala niesie właściwość `sourceUrl` (lista) wskazującą dokładną stronę źródłową.
|
||
- Zachować metadane jakości danych obecne w materiale roboczym (`brak/niepotwierdzona`, `NIEZWERYFIKOWANE`, `brak danych`, rozbieżności źródeł), aby nie utracić kontekstu dziennikarskiego z [research-podlaskie.md](research-podlaskie.md).
|
||
|
||
**Historia (poprzedni plan, superseded)**
|
||
- Pierwsza wersja planu była jednoskryptowa (Python: `requests` + BeautifulSoup, ładowanie przez `neo4j` driver) i ograniczona do województwa podlaskiego. Zastąpiona przez ten plan: **wielowojewódzki**, **warstwowy**, jako aplikacja **Java 21 + Maven + Spring Shell**, gdzie każda warstwa to osobna komenda CLI.
|
||
- Wkład starego planu wchłonięty tutaj: tabela źródeł/provenance (sekcja 3→ obecnie 4.6 + 7), zapytania walidacyjne i raport nakładek (→ 5.6), obsługa przypadków brzegowych i rate-limitu (→ 7), lista produktów i checklista wykonawcza (→ 9, 10). Pseudokod Pythona porzucony — logikę realizuje kod Javy warstw `ingest`/`load`.
|
||
|
||
---
|
||
|
||
## Proces pracy: TDD (obowiązkowy)
|
||
|
||
Cały kod tego planu powstaje test-first. Agent **nie pisze kodu produkcyjnego zanim nie istnieje test, który go wymaga**. Pełne reguły w [CLAUDE.md](CLAUDE.md); tu skrót obowiązujący dla każdego zadania (§11) i każdej klasy z sekcji 3.
|
||
|
||
**Cykl red → green → refactor (małe kroki, jeden zachowaniowy przyrost = jeden obieg):**
|
||
1. **RED** — napisz test opisujący pożądane zachowanie w `src/test/java/...` (mirror pakietu z `src/main/java`). Uruchom `./mvnw -q test -Dtest=NazwaTestu` i zobacz, że **failuje** (kompiluje się, asercja nie przechodzi). Test bez uprzedniej porażki nic nie dowodzi.
|
||
2. **GREEN** — napisz **minimalny** kod produkcyjny zazieleniający test. Bez funkcji, których żaden test nie wymaga.
|
||
3. **REFACTOR** — posprzątaj kod i test przy zielonym pasku; testy nadal zielone.
|
||
|
||
**Zasady wiążące:**
|
||
- **Test najpierw.** Bug fix zaczyna się od testu reprodukującego buga (RED), potem poprawka (np. T1 `Deduplicator`).
|
||
- **Dług techniczny:** istniejący kod produkcyjny powstał bez testów. Dotykając klasy bez pokrycia — najpierw dopisz **charakteryzujący** test aktualnego zachowania (green), potem prowadź zmianę cyklem TDD (patrz T2–T3).
|
||
- **Testy jednostkowe** (bez Neo4j, szybkie, deterministyczne): normalizacja (`NameNormalizer`, `PartyNormalizer`), deduplikacja (`Deduplicator`), parsowanie źródeł (`MarkdownRegistrySource`), serializacja canonical (`CanonicalWriter`/`CanonicalReader`).
|
||
- **Testy integracyjne** dla warstwy `load`: Neo4j przez **Testcontainers** (`org.testcontainers:neo4j`) — idempotencja `MERGE`, constrainty schematu, raporty walidatora (tag **[NEO4J]**, wymaga Dockera).
|
||
- **Nazwy testów** opisują zachowanie: `metoda_warunek_oczekiwanyWynik` (np. `normalize_polishDiacritics_strippedToAsciiSlug`).
|
||
- **Definicja ukończenia:** zielony `./mvnw -q test` jest warunkiem uznania zadania za zrobione. Jeśli testy nie mogą się uruchomić (brak `mvn`/Dockera) — zgłoś jawnie, nie deklaruj sukcesu (tag `[~]` + notatka).
|
||
|
||
---
|
||
|
||
## 1. Architektura warstwowa
|
||
|
||
Aplikacja to jeden artefakt Spring Boot / Spring Shell z dwiema wyraźnie rozdzielonymi warstwami. Warstwy komunikują się przez **plik(i) w uwspólnionym formacie kanonicznym** (JSON), dzięki czemu można je uruchamiać niezależnie, wznawiać i testować.
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ WARSTWA 1: Akwizycja + normalizacja │
|
||
│ komenda: ingest │
|
||
│ wejście: źródła per-województwo (pliki .md/.csv, konektory) │
|
||
│ wyjście: canonical/*.json (Canonical Model, jednolity) │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│ canonical JSON
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ WARSTWA 2: Ładowanie do Neo4j + tworzenie powiązań │
|
||
│ komenda: load │
|
||
│ wejście: canonical/*.json │
|
||
│ wyjście: graf w Neo4j (węzły + relacje + provenance) │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
Rozdział warstw plikiem kanonicznym daje:
|
||
- **niezależność** — warstwę 1 można uruchomić bez działającego Neo4j; warstwę 2 bez dostępu do sieci;
|
||
- **audytowalność** — plik kanoniczny to wersjonowalny, przeglądalny snapshot danych (można trzymać w git);
|
||
- **idempotencję** — warstwa 2 opiera się na `MERGE` po kluczach naturalnych, więc wielokrotne ładowanie tego samego pliku nie tworzy duplikatów.
|
||
|
||
---
|
||
|
||
## 2. Model kanoniczny (kontrakt między warstwami)
|
||
|
||
Jeden schemat dla **wszystkich** województw. Plik `canonical/<voivodeship>.json` (np. `podlaskie.json`, `mazowieckie.json`) lub jeden zbiorczy `canonical/all.json`.
|
||
|
||
```jsonc
|
||
{
|
||
"schemaVersion": "1.0",
|
||
"voivodeship": "podlaskie", // slug województwa (16 możliwych wartości)
|
||
"generatedAt": "2026-07-07T10:00:00Z",
|
||
"hospitals": [
|
||
{
|
||
"id": "hosp:podlaskie:usk-bialystok", // stabilny slug (voiv + nazwa)
|
||
"name": "Uniwersytecki Szpital Kliniczny w Białymstoku",
|
||
"shortName": "USK",
|
||
"city": "Białystok",
|
||
"voivodeship": "podlaskie",
|
||
"legalForm": "SPZOZ", // SPZOZ | SP_PSYCHIATRYCZNY_ZOZ | SPOLKA_Z_OO | ...
|
||
"foundingBody": "Uniwersytet Medyczny w Białymstoku",
|
||
"supervisoryBodyType": "RADA_SPOLECZNA", // RADA_SPOLECZNA | RADA_NADZORCZA
|
||
"nip": null, // gdy dostępny
|
||
"krs": null, // dla spółek
|
||
"website": "https://uskwb.pl",
|
||
"sourceUrls": ["https://uskwb.pl/dyrekcja-szpitala/"]
|
||
}
|
||
],
|
||
"people": [
|
||
{
|
||
"id": "person:jan-kochanowicz", // slug znormalizowanego imienia+nazwiska
|
||
"fullName": "Jan Kochanowicz",
|
||
"displayName": "prof. dr hab. n. med. Jan Kochanowicz", // z tytułami, jak w źródle
|
||
"titles": ["prof.", "dr hab.", "n. med."],
|
||
"sourceUrls": ["https://uskwb.pl/dyrekcja-szpitala/"]
|
||
}
|
||
],
|
||
"affiliations": [ // przynależność partyjna/komitetowa osoby
|
||
{
|
||
"personId": "person:karol-pilecki",
|
||
"type": "PARTIA", // PARTIA | KOMITET_WYBORCZY | KWW_LOKALNY
|
||
"name": "Koalicja Obywatelska",
|
||
"confidence": "POTWIERDZONA", // POTWIERDZONA | NIEPOTWIERDZONA | NIEZWERYFIKOWANA | BRAK_DANYCH
|
||
"note": "jedno źródło prasowe: PO; historycznie PSL",
|
||
"sourceUrls": ["https://bip-spzozwszjs.podlaskie.eu/rada.html"]
|
||
}
|
||
],
|
||
"roles": [ // funkcja osoby w szpitalu (krawędź hosp↔person)
|
||
{
|
||
"hospitalId": "hosp:podlaskie:usk-bialystok",
|
||
"personId": "person:jan-kochanowicz",
|
||
"roleType": "DYREKTOR", // DYREKTOR | ZASTEPCA_DYREKTORA | GLOWNY_KSIEGOWY
|
||
// | CZLONEK_ORGANU_NADZORCZEGO | PRZEWODNICZACY_ORGANU | ...
|
||
"roleLabel": "Dyrektor Naczelny", // oryginalna etykieta ze źródła
|
||
"organ": "DYREKCJA", // DYREKCJA | RADA_SPOLECZNA | RADA_NADZORCZA
|
||
"status": "AKTUALNY", // AKTUALNY | PELNIACY_OBOWIAZKI | ELEKT | BYLY
|
||
"validFrom": null, // ISO data, gdy znana
|
||
"validTo": null,
|
||
"note": "p.o. od 3.03.2025",
|
||
"sourceUrls": ["https://uskwb.pl/dyrekcja-szpitala/"]
|
||
}
|
||
],
|
||
"mandates": [ // mandat samorządowy/publiczny osoby (niezależny od szpitala)
|
||
{
|
||
"personId": "person:marek-olbrys",
|
||
"mandateType": "RADNY_SEJMIKU", // RADNY_SEJMIKU | RADNY_POWIATU | RADNY_GMINY
|
||
// | WOJT_BURMISTRZ_PREZYDENT | STAROSTA | POSEL | SENATOR | MARSZALEK
|
||
"body": "Sejmik Województwa Podlaskiego",
|
||
"term": "VII",
|
||
"voivodeship": "podlaskie",
|
||
"sourceUrls": ["https://www.portalsamorzadowy.pl/osoba/marek-olbrys,876.html"]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**Uwagi projektowe:**
|
||
- `id` osoby jest generowane deterministycznie z znormalizowanego imienia i nazwiska (patrz normalizacja) — to podstawa deduplikacji **między szpitalami i województwami** (np. osoba zasiadająca w radach kilku szpitali).
|
||
- Enumy (`legalForm`, `roleType`, `mandateType`, `confidence`, `status`) są zamknięte i wspólne dla wszystkich województw — to gwarantuje uwspólniony format.
|
||
- Pola `note` i `confidence` przenoszą metadane jakościowe z materiału roboczego (kluczowe dla kontekstu dziennikarskiego).
|
||
|
||
---
|
||
|
||
## 3. Stack technologiczny
|
||
|
||
| Element | Wybór |
|
||
|---|---|
|
||
| Język | Java 21 (records, sealed interfaces, pattern matching, virtual threads) |
|
||
| Build | Maven (multi-module opcjonalnie; na start single-module) |
|
||
| Framework | Spring Boot 3.x + **Spring Shell 3.x** (komendy CLI) |
|
||
| Neo4j | `spring-boot-starter-data-neo4j` **lub** czysty `neo4j-java-driver` (Bolt) |
|
||
| HTTP/scraping | `java.net.http.HttpClient` + **jsoup** (parsowanie HTML) |
|
||
| JSON | Jackson (`jackson-databind` + `jackson-datatype-jsr310`) |
|
||
| Fuzzy-match | `commons-text` (Levenshtein / Jaro-Winkler) do deduplikacji osób |
|
||
| CSV (raporty) | `commons-csv` |
|
||
| Testy | JUnit 5, Testcontainers (`neo4j` container) do testów warstwy ładowania |
|
||
| Konteneryzacja Neo4j | Docker (`neo4j:5`), Bolt na `7687`, UI na `7474` |
|
||
|
||
### Szkielet projektu Maven
|
||
```
|
||
szpitale-graph/
|
||
├── pom.xml
|
||
├── docker-compose.yml # Neo4j 5
|
||
├── src/main/java/com/developx/szpitale/
|
||
│ ├── SzpitaleGraphApplication.java # @SpringBootApplication, entrypoint Spring Shell
|
||
│ ├── model/ # rekordy modelu kanonicznego
|
||
│ │ ├── CanonicalDataset.java
|
||
│ │ ├── Hospital.java Person.java Affiliation.java Role.java Mandate.java
|
||
│ │ └── enums/ (LegalForm, RoleType, MandateType, Confidence, ...)
|
||
│ ├── ingest/ # WARSTWA 1
|
||
│ │ ├── IngestCommand.java # @ShellComponent — komenda `ingest`
|
||
│ │ ├── source/ # źródła per-województwo
|
||
│ │ │ ├── VoivodeshipSource.java # interfejs: List<RawRecord> fetch()
|
||
│ │ │ ├── MarkdownRegistrySource.java# parser materiałów typu research-*.md
|
||
│ │ │ ├── HtmlScraperSource.java # jsoup: strony szpitali/BIP
|
||
│ │ │ └── PkwSource.java # weryfikacja afiliacji w PKW 2024
|
||
│ │ ├── normalize/
|
||
│ │ │ ├── NameNormalizer.java # diakrytyki, tytuły, wielkość liter, slug
|
||
│ │ │ ├── PartyNormalizer.java # mapowanie nazw partii/komitetów
|
||
│ │ │ └── Deduplicator.java # scalanie osób (fuzzy + reguły)
|
||
│ │ └── CanonicalWriter.java # zapis canonical/*.json
|
||
│ └── load/ # WARSTWA 2
|
||
│ ├── LoadCommand.java # @ShellComponent — komenda `load`
|
||
│ ├── CanonicalReader.java # odczyt canonical/*.json
|
||
│ ├── GraphSchema.java # constraints + indeksy (Cypher)
|
||
│ ├── GraphLoader.java # UNWIND/MERGE węzłów i relacji
|
||
│ └── GraphValidator.java # zapytania walidacyjne + raport CSV
|
||
└── src/test/java/... # JUnit + Testcontainers
|
||
```
|
||
|
||
---
|
||
|
||
## 4. WARSTWA 1 — Akwizycja i normalizacja (`ingest`)
|
||
|
||
### 4.1. Zadanie
|
||
Pobrać dane z heterogenicznych źródeł dla wybranego województwa (lub wszystkich) i wyprodukować plik(i) w modelu kanonicznym. **Żadnej zależności od Neo4j.**
|
||
|
||
### 4.2. Interfejs źródła (pluginowalny per województwo)
|
||
```java
|
||
public interface VoivodeshipSource {
|
||
String voivodeship(); // "podlaskie", "mazowieckie", ...
|
||
List<RawHospitalRecord> fetch(); // surowe rekordy przed normalizacją
|
||
}
|
||
```
|
||
Implementacje (wybierane po fladze/konfiguracji):
|
||
- **`MarkdownRegistrySource`** — parsuje istniejące materiały robocze w formacie tabel Markdown (jak `research-podlaskie.md`). To ścieżka startowa: podlaskie już mamy. Kolejne województwa dokładamy jako `research-<voiv>.md` (patrz sekcja 4.6 — generowane skillem deep-research).
|
||
- **`HtmlScraperSource`** — `HttpClient` + jsoup do stron szpitali (dyrekcja) i BIP (rady). Rate-limiting + retry z backoffem, `User-Agent`, poszanowanie `robots.txt`.
|
||
- **`PkwSource`** — kontrola krzyżowa afiliacji radnych w `samorzad2024.pkw.gov.pl` (podnosi `confidence`).
|
||
|
||
Rejestr źródeł spina wszystkie województwa; brak konfiguracji dla danego województwa = pomijane z ostrzeżeniem w logu (żadnych cichych luk).
|
||
|
||
### 4.6. Odkrywanie źródeł dla kolejnych województw — skill `deep-research`
|
||
|
||
Dla podlaskiego materiał źródłowy (`research-podlaskie.md`) już istnieje. Dla pozostałych 15 województw **nie znamy z góry** ani listy szpitali, ani adresów BIP z imiennymi składami rad, ani afiliacji. Ten etap rozpoznawczy realizuje **skill `deep-research`** (fan-out zapytań web, pobranie źródeł, adwersaryjna weryfikacja twierdzeń, synteza raportu z cytowaniami) — jest to krok **poprzedzający** warstwę 1, wykonywany przez agenta, nie przez kod Javy.
|
||
|
||
**Rola:** deep-research pełni funkcję „discovery" — produkuje dla danego województwa materiał `research-<voiv>.md` w **dokładnie tym samym formacie tabel** co `research-podlaskie.md`, który następnie wchłania `MarkdownRegistrySource` bez żadnych zmian w kodzie. To domyka pętlę: skill znajduje i weryfikuje źródła → zapisuje kanoniczny materiał roboczy → warstwa 1 go normalizuje → warstwa 2 ładuje do grafu.
|
||
|
||
**Co zlecić skillowi (per województwo), pytania badawcze:**
|
||
1. **Lista szpitali** danego województwa z podziałem na: wojewódzkie/uniwersyteckie/resortowe (organ tworzący: samorząd województwa / uczelnia medyczna / MSWiA / MON) oraz powiatowe (organ tworzący: powiat), z formą prawną (SPZOZ vs spółka prawa handlowego).
|
||
2. **Kadra kierownicza** każdego szpitala (dyrektor, zastępcy, gł. księgowy) — ze stron „Dyrekcja"/„Kierownictwo" i BIP szpitala.
|
||
3. **Skład organu nadzorczego** — rada społeczna (SPZOZ) lub rada nadzorcza (spółka) — z BIP szpitala / uchwał organu tworzącego.
|
||
4. **Afiliacje polityczne** członków organów — kontrola krzyżowa w PKW 2024 (`samorzad2024.pkw.gov.pl`), na portalsamorzadowy.pl, w BIP sejmiku/rad powiatów; jawne oznaczenie pewności.
|
||
5. **Mandaty samorządowe** (radny sejmiku/powiatu/gminy, wójt/burmistrz/starosta, poseł/senator) osób zasiadających w organach.
|
||
|
||
**Kontrakt wyjścia skilla** — aby był bezpośrednio parsowalny przez `MarkdownRegistrySource`, deep-research musi wyprodukować:
|
||
- nagłówek metodologiczny z datą dostępu i legendą oznaczeń pewności (jak w `research-podlaskie.md`);
|
||
- sekcje per szpital z tabelą `| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL) |`;
|
||
- **`sourceUrl` przy każdym wierszu** (twardy wymóg — brak URL = wiersz odrzucany/oznaczony `BRAK_DANYCH`);
|
||
- markery pewności zgodne z mapowaniem na enum `confidence`: `brak/niepotwierdzona` → `NIEPOTWIERDZONA`, `NIEZWERYFIKOWANE` → `NIEZWERYFIKOWANA`, `brak danych` → `BRAK_DANYCH`;
|
||
- sekcję „Rozbieżności, luki i rekomendacje weryfikacyjne" (jak część III materiału podlaskiego).
|
||
|
||
**Jak wywołać (przykład dla jednego województwa):**
|
||
```
|
||
/deep-research Zbuduj materiał źródłowy o kadrze kierowniczej, radach społecznych/nadzorczych
|
||
i afiliacjach politycznych szpitali w województwie mazowieckim. Zachowaj format
|
||
tabel i legendę oznaczeń pewności identyczne jak w research-podlaskie.md. Przy
|
||
każdym wpisie podaj dokładny URL źródła (BIP szpitala, uchwały organu tworzącego,
|
||
PKW 2024, portalsamorzadowy.pl). Oznacz luki i rozbieżności źródeł.
|
||
```
|
||
Wynik zapisujemy jako `research-mazowieckie.md`. Skalowanie na wszystkie województwa: uruchamiać skill po jednym województwie (koszt i wolumen źródeł są duże) albo w partiach; każde uruchomienie to osobny plik `research-<voiv>.md`.
|
||
|
||
**Granica odpowiedzialności:** deep-research **znajduje i weryfikuje źródła** (etap dziennikarsko-badawczy, wymaga oceny człowieka przed publikacją, zwłaszcza dla oznaczeń `NIEZWERYFIKOWANA`). Kod Javy (warstwa 1) **nie prowadzi researchu** — jedynie deterministycznie normalizuje gotowy materiał `.md` do modelu kanonicznego. Ten podział utrzymuje warstwę 1 testowalną i powtarzalną, a odkrywanie źródeł — audytowalne i cytowane.
|
||
|
||
### 4.3. Normalizacja do formatu uwspólnionego
|
||
1. **Nazwiska** (`NameNormalizer`): usunięcie tytułów do `titles[]`, `displayName` zachowuje oryginał, `fullName` = imię+nazwisko, `id` = slug z NFKD bez diakrytyków, lower-case, `-`. Uwaga na warianty (np. `Wojciech Łuczaj` występuje w kilku szpitalach).
|
||
2. **Partie/komitety** (`PartyNormalizer`): mapowanie na kanoniczne nazwy (`Koalicja Obywatelska`, `PiS`, `PSL`, `Polska 2050`, `Trzecia Droga`, ...) + typ `PARTIA`/`KOMITET_WYBORCZY`/`KWW_LOKALNY`. `confidence` z markerów źródła (`NIEZWERYFIKOWANE` → `NIEZWERYFIKOWANA`, `brak danych` → `BRAK_DANYCH`, `brak/niepotwierdzona` → `NIEPOTWIERDZONA`).
|
||
3. **Funkcje** (`roleType`/`organ`/`status`): mapowanie etykiet („Dyrektor", „p.o. Dyrektora" → status `PELNIACY_OBOWIAZKI`, „Dyrektor-elekt" → `ELEKT`, „Rada Społeczna — Przewodniczący" → `PRZEWODNICZACY_ORGANU` + `organ=RADA_SPOLECZNA`).
|
||
4. **Deduplikacja osób** (`Deduplicator`): scalenie po `id`; przy zbieżnych, ale niepewnych dopasowaniach (pospolite nazwiska: Możejko, Jabłoński, Dzięcioł) — **nie scalać**, dodać `note="możliwy duplikat"` i zachować odrębne węzły. Fuzzy-match (Jaro-Winkler) tylko jako podpowiedź do przeglądu, nie jako automatyczne scalenie.
|
||
|
||
### 4.4. Komenda Spring Shell
|
||
```java
|
||
@ShellComponent
|
||
public class IngestCommand {
|
||
|
||
@ShellMethod(key = "ingest",
|
||
value = "Warstwa 1: pobiera i normalizuje dane szpitali do modelu kanonicznego (JSON).")
|
||
public String ingest(
|
||
@ShellOption(defaultValue = "all") String voivodeship, // np. podlaskie | mazowieckie | all
|
||
@ShellOption(defaultValue = "markdown") String source, // markdown | html | pkw
|
||
@ShellOption(defaultValue = "canonical") String outDir, // katalog wyjściowy
|
||
@ShellOption(defaultValue = "false") boolean split // plik per województwo vs all.json
|
||
) {
|
||
// 1. wybierz źródła dla województw
|
||
// 2. fetch -> normalize -> dedup
|
||
// 3. zapisz canonical/<voiv>.json (lub all.json)
|
||
// 4. zwróć podsumowanie: #szpitali, #osób, #ról, #mandatów, #luk
|
||
}
|
||
}
|
||
```
|
||
Przykłady użycia:
|
||
```
|
||
ingest --voivodeship podlaskie --source markdown
|
||
ingest --voivodeship all --split true
|
||
ingest --voivodeship mazowieckie --source html --out-dir canonical
|
||
```
|
||
|
||
### 4.5. Produkt warstwy 1
|
||
- `canonical/<voiv>.json` (lub `canonical/all.json`) zgodny ze schematem z sekcji 2.
|
||
- `ingest-report.json` — statystyki i lista luk (szpitale bez składu rady, afiliacje `NIEZWERYFIKOWANA`) — odpowiednik „Głównych luk" z materiału roboczego.
|
||
|
||
---
|
||
|
||
## 5. WARSTWA 2 — Ładowanie do Neo4j i tworzenie powiązań (`load`)
|
||
|
||
### 5.1. Zadanie
|
||
Odczytać plik(i) kanoniczne i zmaterializować graf w Neo4j: węzły, relacje i provenance. **Żadnej zależności od sieci/scrapingu.** Idempotentne (`MERGE`).
|
||
|
||
### 5.2. Model grafu
|
||
**Węzły (labels):**
|
||
- `:Hospital {id, name, shortName, city, voivodeship, legalForm, foundingBody, supervisoryBodyType, nip, krs, website, sourceUrls}`
|
||
- `:Person {id, fullName, displayName, titles, sourceUrls}`
|
||
- `:Party {name, type}` (partia/komitet)
|
||
- `:GovBody {name, type, voivodeship, term}` (sejmik, rada powiatu, urząd gminy — dla mandatów)
|
||
- `:Voivodeship {slug, name}` (grupowanie i zapytania regionalne)
|
||
|
||
**Relacje:**
|
||
| Relacja | Od → Do | Właściwości |
|
||
|---|---|---|
|
||
| `[:DYREKTOR]` | Hospital → Person | `roleLabel, status, validFrom, validTo, note, sourceUrls` |
|
||
| `[:ZASTEPCA_DYREKTORA]` | Hospital → Person | j.w. |
|
||
| `[:CZLONEK_ORGANU]` | Hospital → Person | `organ (RADA_SPOLECZNA/RADA_NADZORCZA), roleLabel, status, sourceUrls` |
|
||
| `[:PRZEWODNICZY_ORGANOWI]` | Person → Hospital | `organ, sourceUrls` |
|
||
| `[:CZLONEK_PARTII]` | Person → Party | `confidence, note, sourceUrls` |
|
||
| `[:PELNI_MANDAT]` | Person → GovBody | `mandateType, term, sourceUrls` |
|
||
| `[:W_WOJEWODZTWIE]` | Hospital → Voivodeship | — |
|
||
|
||
> Uwaga: zamiast osobnych typów relacji dla każdej funkcji można użyć jednej `[:PELNI_FUNKCJE {roleType, organ, ...}]`. Wybór: **typowane relacje** dla czytelności zapytań Cypher + `roleType` jako property dla filtrowania.
|
||
|
||
### 5.3. Schemat (constraints + indeksy)
|
||
```cypher
|
||
CREATE CONSTRAINT hospital_id IF NOT EXISTS FOR (h:Hospital) REQUIRE h.id IS UNIQUE;
|
||
CREATE CONSTRAINT person_id IF NOT EXISTS FOR (p:Person) REQUIRE p.id IS UNIQUE;
|
||
CREATE CONSTRAINT party_name IF NOT EXISTS FOR (x:Party) REQUIRE x.name IS UNIQUE;
|
||
CREATE CONSTRAINT voiv_slug IF NOT EXISTS FOR (v:Voivodeship) REQUIRE v.slug IS UNIQUE;
|
||
CREATE INDEX person_name IF NOT EXISTS FOR (p:Person) ON (p.fullName);
|
||
```
|
||
|
||
### 5.4. Ładowanie (parametryzowane, wsadowe)
|
||
```cypher
|
||
UNWIND $hospitals AS h
|
||
MERGE (hosp:Hospital {id: h.id})
|
||
SET hosp += {name:h.name, city:h.city, voivodeship:h.voivodeship,
|
||
legalForm:h.legalForm, supervisoryBodyType:h.supervisoryBodyType,
|
||
website:h.website, sourceUrls:h.sourceUrls}
|
||
MERGE (v:Voivodeship {slug: h.voivodeship})
|
||
MERGE (hosp)-[:W_WOJEWODZTWIE]->(v);
|
||
|
||
UNWIND $people AS p
|
||
MERGE (person:Person {id: p.id})
|
||
SET person += {fullName:p.fullName, displayName:p.displayName,
|
||
titles:p.titles, sourceUrls:p.sourceUrls};
|
||
|
||
UNWIND $roles AS r
|
||
MATCH (h:Hospital {id:r.hospitalId}), (p:Person {id:r.personId})
|
||
CALL apoc.merge.relationship(h, r.relType, {}, r.props, p) YIELD rel // lub CASE per typ bez APOC
|
||
RETURN count(*);
|
||
|
||
UNWIND $affiliations AS a
|
||
MATCH (p:Person {id:a.personId})
|
||
MERGE (party:Party {name:a.name}) SET party.type = a.type
|
||
MERGE (p)-[m:CZLONEK_PARTII]->(party)
|
||
SET m += {confidence:a.confidence, note:a.note, sourceUrls:a.sourceUrls};
|
||
|
||
UNWIND $mandates AS md
|
||
MATCH (p:Person {id:md.personId})
|
||
MERGE (g:GovBody {name:md.body}) SET g.type = md.mandateType, g.voivodeship = md.voivodeship, g.term = md.term
|
||
MERGE (p)-[:PELNI_MANDAT {mandateType:md.mandateType, term:md.term, sourceUrls:md.sourceUrls}]->(g);
|
||
```
|
||
Batche po ~5–10 tys. wierszy w jednej transakcji (tu wolumen mały, ale trzymamy wzorzec).
|
||
> Jeśli nie chcemy zależności od APOC — w `GraphLoader` mapujemy `roleType` na stały typ relacji przez `switch` i wykonujemy osobny sparametryzowany `MERGE` per typ.
|
||
|
||
### 5.5. Komenda Spring Shell
|
||
```java
|
||
@ShellComponent
|
||
public class LoadCommand {
|
||
|
||
@ShellMethod(key = "load",
|
||
value = "Warstwa 2: ładuje model kanoniczny do Neo4j i tworzy powiązania.")
|
||
public String load(
|
||
@ShellOption(defaultValue = "canonical") String inDir, // katalog z canonical/*.json
|
||
@ShellOption(defaultValue = "all") String voivodeship, // filtr: które pliki załadować
|
||
@ShellOption(defaultValue = "true") boolean createSchema,// czy zakładać constraints/indeksy
|
||
@ShellOption(defaultValue = "false") boolean wipe, // wyczyścić graf przed ładowaniem
|
||
@ShellOption(defaultValue = "true") boolean validate // uruchomić walidację po ładowaniu
|
||
) {
|
||
// 1. (opc.) wipe + createSchema
|
||
// 2. read canonical -> UNWIND/MERGE nodes -> relationships
|
||
// 3. (opc.) validate + zapis overlap_report.csv
|
||
// 4. podsumowanie: #węzłów, #relacji per typ
|
||
}
|
||
}
|
||
```
|
||
Konfiguracja połączenia w `application.yml`:
|
||
```yaml
|
||
spring:
|
||
neo4j:
|
||
uri: bolt://localhost:7687
|
||
authentication: { username: neo4j, password: password }
|
||
```
|
||
Przykłady:
|
||
```
|
||
load --in-dir canonical --voivodeship all --wipe true
|
||
load --voivodeship podlaskie --create-schema false
|
||
```
|
||
|
||
### 5.6. Walidacja i raport (`GraphValidator`)
|
||
```cypher
|
||
// Szpitale bez dyrektora (luki do uzupełnienia)
|
||
MATCH (h:Hospital) WHERE NOT (h)-[:DYREKTOR]->() RETURN h.name, h.voivodeship;
|
||
|
||
// Osoby łączące funkcję w organie szpitala z mandatem samorządowym (kluczowy wniosek analityczny)
|
||
MATCH (h:Hospital)-[:CZLONEK_ORGANU]->(p:Person)-[:PELNI_MANDAT]->(g:GovBody)
|
||
RETURN p.fullName, collect(DISTINCT h.name) AS szpitale, collect(DISTINCT g.name) AS mandaty;
|
||
|
||
// Osoby zasiadające w organach wielu szpitali (powiązania międzyszpitalne / międzywojewódzkie)
|
||
MATCH (h:Hospital)-[:CZLONEK_ORGANU]->(p:Person)
|
||
WITH p, collect(DISTINCT h) AS hs WHERE size(hs) > 1
|
||
RETURN p.fullName, [x IN hs | x.name] AS szpitale;
|
||
```
|
||
Eksport `overlap_report.csv` (osoba, szpitale, funkcje, partia, mandaty, `confidence`, `sourceUrls`).
|
||
|
||
---
|
||
|
||
## 6. Rozszerzanie na kolejne województwa
|
||
|
||
Aby dodać województwo (np. mazowieckie):
|
||
1. **Odkrycie źródeł skillem `deep-research`** (sekcja 4.6): wygenerować `research-mazowieckie.md` w formacie tabel identycznym z `research-podlaskie.md` — z URL-ami źródeł, oznaczeniami pewności i sekcją luk/rozbieżności.
|
||
2. Dostarczyć źródło danych warstwie 1: materiał `research-mazowieckie.md` (obsłuży `MarkdownRegistrySource`) lub — dla źródeł ustrukturyzowanych — konfigurację scrapera BIP dla `HtmlScraperSource`.
|
||
3. Zarejestrować źródło w rejestrze województw (bean/konfiguracja) — **bez zmian w warstwie 2**.
|
||
4. Uruchomić `ingest --voivodeship mazowieckie` → `load --voivodeship mazowieckie`.
|
||
|
||
Model kanoniczny, schemat grafu i komendy pozostają niezmienne — nowe województwo to tylko nowe dane w tym samym kontrakcie. Slug województwa jest wymiarem we wszystkich zapytaniach, co umożliwia analizy krajowe i porównania międzyregionalne.
|
||
|
||
---
|
||
|
||
## 7. Obsługa błędów i przypadków brzegowych
|
||
|
||
| Sytuacja | Strategia |
|
||
|---|---|
|
||
| Brak strony ze składem organu | `role`/`affiliation` z `confidence=BRAK_DANYCH`, wpis w `ingest-report.json` (luka) |
|
||
| Identyczne nazwiska, różne osoby | Nie scalać; odrębne węzły + `note` „możliwy duplikat"; fuzzy-match tylko jako podpowiedź |
|
||
| Rozbieżne źródła afiliacji | Zachować obie w `note`, `confidence=NIEZWERYFIKOWANA`, obie w `sourceUrls` |
|
||
| Migracja domen BIP (`wrotapodlasia.pl`→`podlaskie.eu`) | Retry + fallback URL; log nieodpowiadających adresów |
|
||
| Dane dynamiczne (zmiany kadr) | `validFrom`/`validTo` + `status` na relacji funkcji (`AKTUALNY`/`PELNIACY_OBOWIAZKI`/`ELEKT`/`BYLY`) |
|
||
| Rate-limit / blokada scrapera | Exponential backoff; przełączenie na materiał `.md` jako cache; adnotacja źródła |
|
||
| Spółka z o.o. (rada nadzorcza) vs SPZOZ (rada społeczna) | `supervisoryBodyType` + `organ` na relacji rozróżniają organy |
|
||
|
||
---
|
||
|
||
## 8. Uruchomienie (end-to-end)
|
||
|
||
```bash
|
||
# 0. Neo4j
|
||
docker compose up -d # neo4j:5, Bolt 7687, UI 7474
|
||
|
||
# 1. Build
|
||
mvn clean package # Java 21, Spring Boot fat-jar
|
||
|
||
# 2. Uruchom powłokę Spring Shell
|
||
java -jar target/szpitale-graph.jar
|
||
|
||
# 3. W powłoce — warstwa 1, potem warstwa 2
|
||
shell:> ingest --voivodeship all --split true
|
||
shell:> load --voivodeship all --wipe true --validate true
|
||
|
||
# alternatywnie tryb nieinteraktywny (jedna komenda na wywołanie):
|
||
java -jar target/szpitale-graph.jar ingest --voivodeship podlaskie
|
||
java -jar target/szpitale-graph.jar load --voivodeship podlaskie
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Produkty (deliverables)
|
||
|
||
1. **Aplikacja Java 21 / Maven / Spring Shell** z komendami `ingest` (warstwa 1) i `load` (warstwa 2).
|
||
2. **Model kanoniczny** (`canonical/*.json`) — wersjonowalny, wielowojewódzki snapshot danych.
|
||
3. **Schemat Neo4j** — udokumentowane labels, typy relacji i właściwości (sekcja 5.2–5.3).
|
||
4. **`overlap_report.csv`** — osoby łączące funkcje w organach szpitali z mandatami/partiami, z `sourceUrls` i `confidence`.
|
||
5. **`ingest-report.json`** — statystyki i rejestr luk/rozbieżności per województwo.
|
||
6. **Testy** — JUnit + Testcontainers (Neo4j) dla warstwy ładowania; testy jednostkowe normalizacji/deduplikacji.
|
||
|
||
---
|
||
|
||
## 10. Checklista wykonawcza
|
||
|
||
- [ ] `pom.xml`: Java 21, Spring Boot 3.x, Spring Shell, Neo4j driver, jsoup, Jackson, commons-text/csv.
|
||
- [ ] Rekordy modelu kanonicznego + enumy (sekcja 2).
|
||
- [ ] Warstwa 1: `VoivodeshipSource` + `MarkdownRegistrySource` (start: parser `research-podlaskie.md`).
|
||
- [ ] Normalizacja: `NameNormalizer`, `PartyNormalizer`, `Deduplicator`.
|
||
- [ ] Komenda `ingest` + zapis `canonical/*.json` + `ingest-report.json`.
|
||
- [ ] Warstwa 2: `GraphSchema`, `GraphLoader` (UNWIND/MERGE), `GraphValidator`.
|
||
- [ ] Komenda `load` + `overlap_report.csv`.
|
||
- [ ] `docker-compose.yml` (Neo4j 5) + `application.yml` (Bolt, credentials).
|
||
- [ ] Testy: parsowanie MD, normalizacja nazwisk, ładowanie do Testcontainers-Neo4j, zapytania walidacyjne.
|
||
- [ ] Weryfikacja end-to-end na podlaskim, następnie dołożenie kolejnego województwa: `deep-research` → `research-<voiv>.md` → `ingest` → `load`.
|
||
- [ ] Dla każdego nowego województwa: uruchomić skill `deep-research` (sekcja 4.6), zweryfikować oznaczenia `NIEZWERYFIKOWANA` przed użyciem, zapisać `research-<voiv>.md`.
|
||
|
||
---
|
||
|
||
## 11. Kolejka zadań dla lokalnego agenta (TDD, kroki atomowe)
|
||
|
||
Ta sekcja jest pisana pod **mały lokalny model** o ograniczonym kontekście. Każde zadanie jest samowystarczalne: jeden plik lub jedna metoda, dokładna ścieżka, dokładny test, jedna komenda weryfikacji, twarde kryterium ukończenia. Agent **nie planuje** — bierze pierwsze niezrobione zadanie i wykonuje je w całości.
|
||
|
||
### Reguły pracy (przeczytaj przed każdym zadaniem)
|
||
|
||
1. **Bierz DOKŁADNIE jedno zadanie** z listy poniżej, od góry. Nie łącz zadań.
|
||
2. **Nie dotykaj plików spoza zadania.** Jeśli zadanie mówi „plik X", edytuj tylko X i jego test.
|
||
3. **Cykl TDD (obowiązkowy, patrz `CLAUDE.md`):**
|
||
- napisz test w `src/test/java/...` (mirror pakietu),
|
||
- uruchom `./mvnw -q test -Dtest=NazwaTestu` i zobacz **RED** (czerwony),
|
||
- napisz minimalny kod produkcyjny → **GREEN**,
|
||
- posprzątaj przy zielonym → **REFACTOR**.
|
||
4. **Komenda z katalogu `szpitale-graph/`.** Zawsze `./mvnw` (jest wrapper), nigdy `mvn`.
|
||
5. **Definicja ukończenia zadania:** `./mvnw -q test` przechodzi na zielono ORAZ nie ma nowych ostrzeżeń kompilatora dla dotkniętego pliku.
|
||
6. **Jeśli utkniesz > 2 próby** — zostaw plik bez zmian, dopisz jednolinijkową notatkę `// TODO(local-agent): <opis blokady>` i przejdź dalej NIE psując builda.
|
||
7. **Nie wymyślaj API.** Używaj tylko klas/metod, które już istnieją w `src/main/java` (sprawdź plik zanim wywołasz metodę).
|
||
8. Zadania z tagiem **[NEO4J]** wymagają Dockera (Testcontainers). Jeśli `docker` niedostępny — pomiń, zostaw zadanie odhaczone jako `[~]` z notatką „brak Dockera".
|
||
9. **Zapisz punkt kontrolny po KAŻDYM zadaniu** (patrz §13). Bez zapisanego checkpointu zadanie NIE jest ukończone, nawet jeśli testy są zielone.
|
||
|
||
### Legenda statusów: `[ ]` niezrobione · `[x]` zrobione · `[~]` pominięte (z notatką)
|
||
|
||
---
|
||
|
||
**T1 — Naprawić kompilację w `Deduplicator` [BLOKER, robić pierwsze]**
|
||
- Plik: `src/main/java/com/developx/szpitale/ingest/normalize/Deduplicator.java`, metoda `checkFuzzyDuplicates`.
|
||
- Objaw: `groups` jest typu `Map<String, List<Person>>`, a kod wkłada do niego `List<String>` (błąd kompilacji ~linia 75). Zmienna `personIds` jest nieużywana, a pętla `for (String existingId : groups.keySet())` iteruje po pustej mapie — nigdy nie porówna z już-widzianymi osobami.
|
||
- Zamiar metody: dla każdej osoby znaleźć **już przetworzone** osoby o podobnym nazwisku (Jaro-Winkler w przedziale `[SUSPECT_THRESHOLD, DUPLICATE_THRESHOLD)`) i wypisać ostrzeżenie o możliwych duplikatach. **Nie scalać** (patrz sekcja 4.3 pkt 4).
|
||
- TDD:
|
||
1. Test `DeduplicatorTest.checkFuzzyDuplicates_similarNames_reportedNotMerged`: podaj listę 2 osób o bardzo podobnych `fullName` (np. `Jan Kowalski` / `Jan Kowaski`) o różnych `id`; asercja: obie osoby nadal istnieją jako odrębne węzły (brak scalenia). RED najpierw.
|
||
2. Minimalny fix: zmień typ na `Map<String, List<String>>` (id → lista podobnych id) i porównuj bieżącą osobę z **wcześniej odwiedzonymi** (np. utrzymuj `List<Person> seen` i iteruj po niej), a nie po `groups.keySet()`. Popraw wypis, by używał `candidates`, nie `personIds`; usuń `personIds`.
|
||
- DoD: `./mvnw -q compile` przechodzi; `./mvnw -q test -Dtest=DeduplicatorTest` zielony.
|
||
|
||
**T2 — Testy `NameNormalizer` (charakteryzujące + slug)**
|
||
- Plik prod: `src/main/java/com/developx/szpitale/ingest/normalize/NameNormalizer.java` (istnieje — najpierw go przeczytaj, nazwy metod bierz z pliku).
|
||
- Test: `NameNormalizerTest` w `src/test/java/com/developx/szpitale/ingest/normalize/`.
|
||
- Przypadki (po jednym asercie na test):
|
||
- `normalize_polishDiacritics_strippedToAsciiSlug` → wejście `Łukasz Żółć` daje slug bez diakrytyków, lower-case, separator `-` (np. `lukasz-zolc`).
|
||
- `normalize_academicTitles_movedToTitlesList` → `prof. dr hab. Jan Kochanowicz` → `fullName == "Jan Kochanowicz"`, `titles` zawiera `prof.`, `dr hab.`.
|
||
- `normalize_displayName_keepsOriginal` → `displayName` zachowuje oryginał z tytułami.
|
||
- DoD: `./mvnw -q test -Dtest=NameNormalizerTest` zielony. Jeśli zachowanie kodu odbiega od oczekiwań — to jest RED; napraw kod minimalnie, chyba że wymagałoby to zmiany API (wtedy `// TODO(local-agent)` i pomiń dany przypadek).
|
||
|
||
**T3 — Testy `PartyNormalizer` (mapowanie nazw + confidence)**
|
||
- Plik prod: `.../ingest/normalize/PartyNormalizer.java` (przeczytaj metody).
|
||
- Test: `PartyNormalizerTest`.
|
||
- Przypadki:
|
||
- `mapParty_knownAlias_canonicalName` → `PO` / `KO` → `Koalicja Obywatelska` (użyj aliasu, który realnie jest w mapie w kodzie).
|
||
- `mapConfidence_marker_toEnum` → marker `NIEZWERYFIKOWANE` → `ConfidenceLevel.NIEZWERYFIKOWANA`; `brak danych` → `BRAK_DANYCH`; `brak/niepotwierdzona` → `NIEPOTWIERDZONA`.
|
||
- DoD: `./mvnw -q test -Dtest=PartyNormalizerTest` zielony.
|
||
|
||
**T4 — Test `MarkdownRegistrySource` na małym fixture**
|
||
- Plik prod: `.../ingest/source/MarkdownRegistrySource.java` (przeczytaj sygnaturę `fetch()` / konstruktor).
|
||
- Fixture: utwórz `src/test/resources/fixtures/research-mini.md` z JEDNYM szpitalem i tabelą `| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL) |` z 1 wierszem zawierającym URL.
|
||
- Test: `MarkdownRegistrySourceTest.fetch_miniFixture_returnsOneHospitalRecord` → asercja: dokładnie 1 `RawHospitalRecord`, z niepustym `sourceUrl`.
|
||
- Zasada twarda (sekcja 4.6): **wiersz bez URL** → odrzucony lub oznaczony `BRAK_DANYCH`. Dodaj drugi test `fetch_rowWithoutUrl_flaggedOrSkipped` na to.
|
||
- DoD: `./mvnw -q test -Dtest=MarkdownRegistrySourceTest` zielony.
|
||
|
||
**T5 — Round-trip `CanonicalWriter` → `CanonicalReader`**
|
||
- Pliki prod: `.../ingest/CanonicalWriter.java`, `.../load/CanonicalReader.java`.
|
||
- Test: `CanonicalRoundTripTest.write_thenRead_yieldsEqualDataset` → zbuduj mały `CanonicalDataset` (1 szpital, 1 osoba, 1 rola), zapisz do pliku tymczasowego (`@TempDir`), odczytaj i porównaj pola. RED najpierw (może ujawnić brak modułu JSR-310 lub złą serializację enumów).
|
||
- DoD: `./mvnw -q test -Dtest=CanonicalRoundTripTest` zielony.
|
||
|
||
**T6 — [NEO4J] Idempotencja `GraphLoader` (Testcontainers)**
|
||
- Pliki prod: `.../load/GraphSchema.java`, `.../load/GraphLoader.java`.
|
||
- Test integracyjny: `GraphLoaderIT` z `@Testcontainers` + `Neo4jContainer` (dependency już w `pom.xml`).
|
||
- Przypadki:
|
||
- `load_twice_isIdempotent` → załaduj ten sam mały dataset 2× i sprawdź, że liczba węzłów `:Hospital`/`:Person` się nie podwaja (skutek `MERGE`).
|
||
- `schema_constraints_created` → po `GraphSchema` istnieje constraint unikalności na `Hospital.id`.
|
||
- Wymaga Dockera. Bez Dockera → `[~]` + notatka.
|
||
- DoD: `./mvnw -q test -Dtest=GraphLoaderIT` zielony (lub pominięte z notatką).
|
||
|
||
**T7 — [NEO4J] Zapytania walidatora + `overlap_report.csv`**
|
||
- Plik prod: `.../load/GraphValidator.java`.
|
||
- Test integracyjny `GraphValidatorIT`: wgraj dataset, w którym jedna osoba ma i `CZLONEK_ORGANU` (szpital) i `PELNI_MANDAT` (GovBody); asercja: raport nakładek zawiera tę osobę; plik `overlap_report.csv` powstaje i ma nagłówek z sekcji 5.6.
|
||
- DoD: `./mvnw -q test -Dtest=GraphValidatorIT` zielony (lub pominięte z notatką).
|
||
|
||
**T8 — Zielony pełny build**
|
||
- Uruchom `./mvnw -q test` (wszystko). Cel: brak błędów, brak nowych ostrzeżeń.
|
||
- Jeśli któreś [NEO4J] pominięte — odnotuj w tej checklistcie jako `[~]` z powodem.
|
||
- DoD: `./mvnw -q verify` przechodzi (pomijając wyłącznie zadania bez Dockera).
|
||
|
||
---
|
||
|
||
## 12. Sekcja postępu (aktualizuje lokalny agent)
|
||
|
||
Po ukończeniu zadania agent zmienia jego status w liście poniżej i dopisuje jedną linię do dziennika.
|
||
|
||
- [ ] T1 Deduplicator compile fix
|
||
- [ ] T2 NameNormalizer testy
|
||
- [ ] T3 PartyNormalizer testy
|
||
- [ ] T4 MarkdownRegistrySource test
|
||
- [ ] T5 Canonical round-trip
|
||
- [ ] T6 GraphLoader idempotencja [NEO4J]
|
||
- [ ] T7 GraphValidator + CSV [NEO4J]
|
||
- [ ] T8 Zielony pełny build
|
||
|
||
**Dziennik (data — zadanie — wynik):**
|
||
- (pusto)
|
||
|
||
---
|
||
|
||
## 13. Punkty kontrolne i wznawianie (odporność na śmierć agenta)
|
||
|
||
Cel: jeśli lokalny agent zostanie ubity w połowie (crash, wyczerpany kontekst, restart), **następne uruchomienie musi kontynuować od ostatniego bezpiecznego stanu**, bez powtarzania zrobionej pracy i bez zostawiania połowicznych zmian.
|
||
|
||
Mechanizm opiera się na dwóch trwałych źródłach prawdy:
|
||
- **git** — jeden commit = jeden ukończony punkt kontrolny (kod jest w historii, nie tylko w drzewie roboczym);
|
||
- **`local-agent-progress.json`** (w katalogu `szpitale-graph/`) — maszynowy stan zadań, czytany na starcie.
|
||
|
||
### 13.1. Procedura STARTU (zawsze na początku uruchomienia)
|
||
|
||
1. `cd szpitale-graph`
|
||
2. Przeczytaj `local-agent-progress.json`.
|
||
3. `git status --porcelain` — sprawdź drzewo robocze:
|
||
- **Czyste** → wznów od pierwszego zadania o statusie `todo`.
|
||
- **Brudne** (są niezacommitowane zmiany) → oznacza to przerwanie w połowie zadania `in_progress`. Wykonaj **odzysk**: `git checkout -- . && git clean -fd` (odrzuć połowiczną pracę), ustaw to zadanie z powrotem na `todo` w JSON, i zacznij je od zera. Połowiczny kod jest zawsze do wyrzucenia, bo checkpoint zapisujemy tylko przy zielonych testach.
|
||
4. Zweryfikuj punkt startowy: `./mvnw -q test` powinno odzwierciedlać ostatni checkpoint (zielone dla zrobionych zadań; czerwone tylko dla T1 na samym starcie projektu).
|
||
|
||
### 13.2. Procedura na WEJŚCIU w zadanie
|
||
|
||
1. Ustaw status zadania w JSON na `in_progress`, wpisz `startedAt` (data z systemu).
|
||
2. **Zacommituj samą tę zmianę stanu**: `git add local-agent-progress.json && git commit -m "T<n>: start"`. Dzięki temu drzewo jest czyste w trakcie pracy nad kodem, a przerwanie łatwo wykryć (jakiekolwiek brudne pliki = połowiczna praca do odrzucenia).
|
||
|
||
### 13.3. Procedura ZAPISU punktu kontrolnego (po zielonych testach)
|
||
|
||
Wykonuj dopiero gdy `./mvnw -q test` (lub `-Dtest=...` dla danego zadania) jest **zielone**:
|
||
1. Zaktualizuj `local-agent-progress.json`: status `done`, `finishedAt`, krótka `note`.
|
||
2. Zaktualizuj checklistę w §12 (`[ ]` → `[x]` / `[~]`) i dopisz linię do dziennika §12.
|
||
3. Jeden atomowy commit obejmujący kod + testy + JSON + PLAN:
|
||
```
|
||
git add -A && git commit -m "T<n>: <krótki opis> [checkpoint]"
|
||
```
|
||
4. Po commicie drzewo jest czyste → to jest bezpieczny punkt wznowienia.
|
||
|
||
### 13.4. Niezmienniki (muszą zachodzić między zadaniami)
|
||
|
||
- **Każdy commit buduje się** — nigdy nie commituj czerwonego builda (wyjątek: jedyny dozwolony czerwony stan to punkt WYJŚCIOWY projektu przed T1).
|
||
- **Jeden checkpoint = jedno zadanie** — nie łącz dwóch zadań w jeden commit.
|
||
- **Brudne drzewo == praca w toku do odrzucenia** — nigdy nie da się „częściowo ukończonego" zadania odzyskać; zawsze restart zadania od zera. To celowe uproszczenie: agent nie musi rozumować o połowicznym stanie.
|
||
- **JSON i git zgadzają się** — jeśli JSON mówi `done` a `git log` nie ma checkpointu tego zadania (lub odwrotnie), źródłem prawdy jest **git**: napraw JSON do stanu wynikającego z historii commitów, potem kontynuuj.
|
||
|
||
### 13.5. Format `local-agent-progress.json`
|
||
|
||
Pełny bieżący stan jest w pliku `szpitale-graph/local-agent-progress.json`. Schemat pola `tasks[]`:
|
||
- `id` (np. `"T1"`), `title`, `status` (`todo` | `in_progress` | `done` | `skipped`),
|
||
- `startedAt`, `finishedAt` (ISO-8601 lub `null`), `note` (string).
|
||
|
||
Wznawianie sprowadza się do: wczytaj JSON → znajdź pierwszy `todo`/`in_progress` → (jeśli `in_progress` + brudne drzewo) odzysk wg §13.1 → wykonuj wg §13.2–13.3.
|
||
|
||
---
|
||
|
||
**Koniec planu**
|