Compare commits

...
13 Commits
87 changed files with 1587 additions and 1443 deletions
+1
View File
@@ -0,0 +1 @@
/.idea/
+196 -14
View File
@@ -8,6 +8,8 @@
**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`.
- `PLAN2.md` (delta) wchłonięty tutaj: rozszerzenie `Person` o `title`/`pwzNumber` (→ 2, 4.7), nowe encje `Organization`/`Company` z powiązaniami po KRS/NIP/CEIDG (→ 2, 4.8, 5.2), nowe zadania kolejki agenta (→ T9T13). Plik `PLAN2.md` usunięty po scaleniu.
- Kolejka §11 przepisana pod konkretny model wykonawczy: **Qwen-3.6-35B-A3B** (MoE, ~3B aktywnych parametrów na token, mały kontekst efektywny). Reguły atomowości zadań i zakaz planowania (już obecne w poprzedniej wersji dla „małego lokalnego modelu") pozostają — dopisano jawne odniesienie do tego modelu, by przyszłe zadania kalibrować pod jego ograniczenia kontekstu i skłonność do zmyślania API.
---
@@ -89,9 +91,57 @@ Jeden schemat dla **wszystkich** województw. Plik `canonical/<voivodeship>.json
"fullName": "Jan Kochanowicz",
"displayName": "prof. dr hab. n. med. Jan Kochanowicz", // z tytułami, jak w źródle
"titles": ["prof.", "dr hab.", "n. med."],
"title": "prof. dr hab. n. med.", // tytuł skondensowany (jedno pole, do wyświetlania obok fullName)
"pwzNumber": "1234567", // Prawo Wykonywania Zawodu, z rejestr.nil.org.pl; null gdy nieznaleziony
"sourceUrls": ["https://uskwb.pl/dyrekcja-szpitala/"]
}
],
"organizations": [ // NGO/fundacje/stowarzyszenia powiązane z osobami przez KRS
{
"id": "org:fundacja-xyz",
"name": "Fundacja XYZ",
"krs": "0000123456",
"nip": null,
"sourceUrls": ["https://ems.ms.gov.pl/..."]
}
],
"companies": [ // spółki/działalności gospodarcze powiązane z osobami
{
"id": "company:abc-sp-zoo",
"name": "ABC sp. z o.o.",
"krs": "0000654321",
"nip": "1234567890",
"ceidgId": null,
"sourceUrls": ["https://wyszukiwarka-krs.ms.gov.pl/..."]
}
],
"personOrganizationLinks": [ // osoba ↔ organizacja, dopasowanie po KRS
{
"personId": "person:jan-kochanowicz",
"organizationId": "org:fundacja-xyz",
"roleLabel": "Prezes Zarządu",
"basis": "KRS", // klucz, po którym dopasowano powiązanie
"sourceUrls": ["https://ems.ms.gov.pl/..."]
}
],
"organizationLinks": [ // organizacja ↔ organizacja, po KRS/NIP (np. wspólny adres/zarząd)
{
"fromOrganizationId": "org:fundacja-xyz",
"toOrganizationId": "org:inna-fundacja",
"basis": "NIP", // KRS | NIP
"note": "wspólny adres siedziby",
"sourceUrls": ["https://ems.ms.gov.pl/..."]
}
],
"personCompanyLinks": [ // osoba ↔ firma, po KRS/CEIDG/NIP
{
"personId": "person:jan-kochanowicz",
"companyId": "company:abc-sp-zoo",
"roleLabel": "Wspólnik",
"basis": "KRS", // KRS | CEIDG | NIP
"sourceUrls": ["https://wyszukiwarka-krs.ms.gov.pl/..."]
}
],
"affiliations": [ // przynależność partyjna/komitetowa osoby
{
"personId": "person:karol-pilecki",
@@ -133,8 +183,10 @@ Jeden schemat dla **wszystkich** województw. Plik `canonical/<voivodeship>.json
**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.
- Enumy (`legalForm`, `roleType`, `mandateType`, `confidence`, `status`) są zamknięte i wspólne dla wszystkich województw — to gwarantuje uwspólniony format. Nowy zamknięty enum `LinkBasis` (`KRS`, `NIP`, `CEIDG`) opisuje klucz dopasowania powiązań organizacja/firma.
- Pola `note` i `confidence` przenoszą metadane jakościowe z materiału roboczego (kluczowe dla kontekstu dziennikarskiego).
- `id` organizacji/firmy jest slugiem z nazwy + (gdy dostępny) KRS w nawiasie logiki dedupu — dwie różne organizacje o tej samej nazwie, ale różnym KRS, **nie są scalane** (analogicznie do zasady dla `Person` w 4.3 pkt 4).
- `pwzNumber` bywa `null` — brak trafienia w rejestr.nil.org.pl to nie błąd, tylko luka odnotowywana w `ingest-report.json` (patrz 4.7).
---
@@ -152,6 +204,7 @@ Jeden schemat dla **wszystkich** województw. Plik `canonical/<voivodeship>.json
| 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` |
| Rejestry zewnętrzne | rejestr.nil.org.pl (PWZ, formularz HTML — brak API), wyszukiwarka-krs.ms.gov.pl / api-krs (KRS), CEIDG API (dane.biznes.gov.pl) |
### Szkielet projektu Maven
```
@@ -163,14 +216,18 @@ szpitale-graph/
│ ├── model/ # rekordy modelu kanonicznego
│ │ ├── CanonicalDataset.java
│ │ ├── Hospital.java Person.java Affiliation.java Role.java Mandate.java
│ │ ── enums/ (LegalForm, RoleType, MandateType, Confidence, ...)
│ │ ── Organization.java Company.java PersonOrganizationLink.java OrganizationLink.java PersonCompanyLink.java
│ │ └── enums/ (LegalForm, RoleType, MandateType, Confidence, LinkBasis, ...)
│ ├── 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
│ │ │ ── PkwSource.java # weryfikacja afiliacji w PKW 2024
│ │ │ ├── PwzRegistrySource.java # uzupełnienie pwzNumber z rejestr.nil.org.pl (4.7)
│ │ │ ├── KrsSource.java # organizacje/firmy + powiązania po KRS (4.8)
│ │ │ └── CeidgSource.java # powiązania osoba↔firma po CEIDG/NIP (4.8)
│ │ ├── normalize/
│ │ │ ├── NameNormalizer.java # diakrytyki, tytuły, wielkość liter, slug
│ │ │ ├── PartyNormalizer.java # mapowanie nazw partii/komitetów
@@ -203,6 +260,9 @@ 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`).
- **`PwzRegistrySource`** — uzupełnia `pwzNumber` osoby z `rejestr.nil.org.pl` (formularz imię+nazwisko, patrz 4.7).
- **`KrsSource`** — wyszukuje organizacje/firmy powiązane z osobą po KRS (patrz 4.8).
- **`CeidgSource`** — kontrola krzyżowa działalności gospodarczych osoby po CEIDG/NIP (patrz 4.8).
Rejestr źródeł spina wszystkie województwa; brak konfiguracji dla danego województwa = pomijane z ostrzeżeniem w logu (żadnych cichych luk).
@@ -238,6 +298,27 @@ Wynik zapisujemy jako `research-mazowieckie.md`. Skalowanie na wszystkie wojewó
**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.7. Uzupełnienie numeru PWZ — `PwzRegistrySource`
`rejestr.nil.org.pl` (Naczelna Izba Lekarska) udostępnia wyszukiwarkę lekarzy **wyłącznie jako formularz HTML** (imię + nazwisko), bez publicznego API. Wzbogacenie działa jako osobny krok w `ingest`, **po** normalizacji osób (potrzebuje `fullName`):
1. Dla każdej osoby o `titles` sugerujących zawód medyczny (`dr`, `dr n. med.`, `prof.`, `lek.`, itp. — heurystyka w `PwzRegistrySource`, nie każda osoba w radzie jest lekarzem) wyślij zapytanie z `fullName`.
2. **Wiele trafień** (częste nazwiska) → `pwzNumber = null`, `note = "wieloznaczne trafienie NIL, wymaga ręcznej weryfikacji"`, wpis w `ingest-report.json`.
3. **Brak trafienia**`pwzNumber = null`, brak `note` (osoba może nie być lekarzem — to nie błąd).
4. Rate-limiting jak dla `HtmlScraperSource` (4.2/7) — to ten sam serwis żywy, nie plik `.md`.
**Granica odpowiedzialności:** to wzbogacenie jest **deterministyczne i automatyczne** (1:1 zapytanie→wynik po nazwisku), inaczej niż `deep-research` (4.6) — nie wymaga oceny dziennikarskiej, tylko technicznej weryfikacji przy wieloznaczności.
### 4.8. Organizacje i firmy — `KrsSource` / `CeidgSource`
Rozszerzenie modelu o encje `Organization` (fundacje, stowarzyszenia) i `Company` (spółki, działalności gospodarcze) powiązane z osobami z organów szpitali — cel: ujawnić potencjalne konflikty interesów.
1. **`KrsSource`** — dla każdej osoby: wyszukanie w KRS (`wyszukiwarka-krs.ms.gov.pl` / `api-krs`) wpisów, gdzie figuruje jako zarząd/wspólnik/fundator. Zwraca `Organization`/`Company` + `personOrganizationLinks`/`personCompanyLinks` z `basis=KRS`.
2. Powiązania **organizacja↔organizacja** (`organizationLinks`) — wspólny KRS-owy adres, zarząd lub `nip` między dwiema organizacjami; `basis=KRS` lub `basis=NIP`.
3. **`CeidgSource`** — kontrola krzyżowa jednoosobowych działalności gospodarczych osoby po CEIDG (`dane.biznes.gov.pl`, API publiczne) i po `nip`; `basis=CEIDG` lub `basis=NIP`.
4. Deduplikacja: `Organization`/`Company` scalane po `krs` (klucz twardy, jednoznaczny) — w przeciwieństwie do `Person`, gdzie deduplikacja jest fuzzy i ostrożna (4.3 pkt 4), bo KRS/NIP są identyfikatorami unikalnymi z rejestru państwowego.
5. Brak dopasowania po KRS/CEIDG/NIP dla danej osoby = brak wpisu (nie każda osoba w radzie ma powiązania biznesowe) — nie jest to luka do raportowania.
### 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`).
@@ -285,10 +366,12 @@ Odczytać plik(i) kanoniczne i zmaterializować graf w Neo4j: węzły, relacje i
### 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}`
- `:Person {id, fullName, displayName, titles, title, pwzNumber, 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)
- `:Organization {id, name, krs, nip, sourceUrls}` (fundacje, stowarzyszenia)
- `:Company {id, name, krs, nip, ceidgId, sourceUrls}` (spółki, działalności gospodarcze)
**Relacje:**
| Relacja | Od → Do | Właściwości |
@@ -300,6 +383,9 @@ Odczytać plik(i) kanoniczne i zmaterializować graf w Neo4j: węzły, relacje i
| `[:CZLONEK_PARTII]` | Person → Party | `confidence, note, sourceUrls` |
| `[:PELNI_MANDAT]` | Person → GovBody | `mandateType, term, sourceUrls` |
| `[:W_WOJEWODZTWIE]` | Hospital → Voivodeship | — |
| `[:CZLONEK_ORGANIZACJI]` | Person → Organization | `roleLabel, basis (KRS), sourceUrls` |
| `[:POWIAZANA_Z]` | Organization → Organization | `basis (KRS/NIP), note, sourceUrls` |
| `[:POWIAZANY_Z_FIRMA]` | Person → Company | `roleLabel, basis (KRS/CEIDG/NIP), sourceUrls` |
> 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.
@@ -309,7 +395,10 @@ CREATE CONSTRAINT hospital_id IF NOT EXISTS FOR (h:Hospital) REQUIRE h.id IS UNI
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 CONSTRAINT org_id IF NOT EXISTS FOR (o:Organization) REQUIRE o.id IS UNIQUE;
CREATE CONSTRAINT company_id IF NOT EXISTS FOR (c:Company) REQUIRE c.id IS UNIQUE;
CREATE INDEX person_name IF NOT EXISTS FOR (p:Person) ON (p.fullName);
CREATE INDEX person_pwz IF NOT EXISTS FOR (p:Person) ON (p.pwzNumber);
```
### 5.4. Ładowanie (parametryzowane, wsadowe)
@@ -325,7 +414,30 @@ 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};
titles:p.titles, title:p.title, pwzNumber:p.pwzNumber, sourceUrls:p.sourceUrls};
UNWIND $organizations AS o
MERGE (org:Organization {id: o.id})
SET org += {name:o.name, krs:o.krs, nip:o.nip, sourceUrls:o.sourceUrls};
UNWIND $companies AS c
MERGE (comp:Company {id: c.id})
SET comp += {name:c.name, krs:c.krs, nip:c.nip, ceidgId:c.ceidgId, sourceUrls:c.sourceUrls};
UNWIND $personOrganizationLinks AS pol
MATCH (p:Person {id:pol.personId}), (org:Organization {id:pol.organizationId})
MERGE (p)-[m:CZLONEK_ORGANIZACJI]->(org)
SET m += {roleLabel:pol.roleLabel, basis:pol.basis, sourceUrls:pol.sourceUrls};
UNWIND $organizationLinks AS ol
MATCH (a:Organization {id:ol.fromOrganizationId}), (b:Organization {id:ol.toOrganizationId})
MERGE (a)-[m:POWIAZANA_Z]->(b)
SET m += {basis:ol.basis, note:ol.note, sourceUrls:ol.sourceUrls};
UNWIND $personCompanyLinks AS pcl
MATCH (p:Person {id:pcl.personId}), (comp:Company {id:pcl.companyId})
MERGE (p)-[m:POWIAZANY_Z_FIRMA]->(comp)
SET m += {roleLabel:pcl.roleLabel, basis:pcl.basis, sourceUrls:pcl.sourceUrls};
UNWIND $roles AS r
MATCH (h:Hospital {id:r.hospitalId}), (p:Person {id:r.personId})
@@ -393,8 +505,15 @@ RETURN p.fullName, collect(DISTINCT h.name) AS szpitale, collect(DISTINCT g.name
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;
// Osoby w organie szpitala mające jednocześnie powiązanie biznesowe (konflikt interesów)
MATCH (h:Hospital)-[:CZLONEK_ORGANU]->(p:Person)
WHERE (p)-[:POWIAZANY_Z_FIRMA]->() OR (p)-[:CZLONEK_ORGANIZACJI]->()
OPTIONAL MATCH (p)-[:POWIAZANY_Z_FIRMA]->(comp:Company)
OPTIONAL MATCH (p)-[:CZLONEK_ORGANIZACJI]->(org:Organization)
RETURN p.fullName, h.name, collect(DISTINCT comp.name) AS firmy, collect(DISTINCT org.name) AS organizacje;
```
Eksport `overlap_report.csv` (osoba, szpitale, funkcje, partia, mandaty, `confidence`, `sourceUrls`).
Eksport `overlap_report.csv` (osoba, szpitale, funkcje, partia, mandaty, firmy/organizacje, `confidence`, `sourceUrls`).
---
@@ -421,6 +540,9 @@ Model kanoniczny, schemat grafu i komendy pozostają niezmienne — nowe wojewó
| 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 |
| Wieloznaczne trafienie w rejestr.nil.org.pl (popularne nazwisko) | `pwzNumber=null`, `note="wieloznaczne trafienie NIL, wymaga ręcznej weryfikacji"`, wpis w `ingest-report.json` |
| Brak trafienia w KRS/CEIDG dla osoby | Brak wpisu w `personOrganizationLinks`/`personCompanyLinks` — to nie luka (nie każdy ma powiązania biznesowe) |
| Ta sama nazwa organizacji, różny KRS | Osobne węzły `:Organization` (dedup po `krs`, nie po nazwie) |
---
@@ -452,9 +574,10 @@ java -jar target/szpitale-graph.jar load --voivodeship podlaskie
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.25.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.
4. **`overlap_report.csv`** — osoby łączące funkcje w organach szpitali z mandatami/partiami/firmami/organizacjami, z `sourceUrls` i `confidence`.
5. **`ingest-report.json`** — statystyki i rejestr luk/rozbieżności per województwo (w tym wieloznaczne trafienia PWZ).
6. **Testy** — JUnit + Testcontainers (Neo4j) dla warstwy ładowania; testy jednostkowe normalizacji/deduplikacji.
7. **Encje `Organization`/`Company`** w modelu kanonicznym i grafie, z powiązaniami do osób po KRS/CEIDG/NIP (sekcja 4.8, 5.2).
---
@@ -471,12 +594,15 @@ java -jar target/szpitale-graph.jar load --voivodeship podlaskie
- [ ] 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`.
- [ ] `Person` rozszerzony o `title`/`pwzNumber` (sekcja 2, 4.7) + `PwzRegistrySource`.
- [ ] Nowe rekordy modelu: `Organization`, `Company`, `PersonOrganizationLink`, `OrganizationLink`, `PersonCompanyLink` (sekcja 2) + `KrsSource`/`CeidgSource` (sekcja 4.8).
- [ ] Warstwa 2: węzły `:Organization`/`:Company` + relacje `CZLONEK_ORGANIZACJI`/`POWIAZANA_Z`/`POWIAZANY_Z_FIRMA` (sekcja 5.2, 5.4) + constrainty (5.3).
---
## 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.
Ta sekcja jest pisana pod lokalny model wykonawczy **Qwen-3.6-35B-A3B** (MoE, ~3B aktywnych parametrów na token, mały efektywny kontekst, skłonność do zmyślania nieistniejących API przy niedoprecyzowanym zadaniu). 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. Zasada 7 (poniżej) jest przy tym modelu szczególnie krytyczna: A3B chętnie "dopisuje" wygodne metody, których nie ma w `src/main/java` — zawsze czytaj plik przed wywołaniem metody.
### Reguły pracy (przeczytaj przed każdym zadaniem)
@@ -555,6 +681,47 @@ Ta sekcja jest pisana pod **mały lokalny model** o ograniczonym kontekście. Ka
- 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).
**T9 — Rozszerzyć `Person` o `title` i `pwzNumber`**
- Plik: `src/main/java/com/developx/szpitale/model/Person.java` (rekord z 5 polami: `id, fullName, displayName, titles, sourceUrls`).
- Zamiar: dodać dwa nowe pola rekordu: `@JsonProperty("title") String title` (może być `null`) i `@JsonProperty("pwzNumber") String pwzNumber` (może być `null`), jako **ostatnie** dwa parametry konstruktora rekordu.
- TDD:
1. Test `PersonTest.person_withTitleAndPwzNumber_fieldsAccessible` w `src/test/java/com/developx/szpitale/model/PersonTest.java`: zbuduj `Person` z niepustym `title` i `pwzNumber`, asercja że `person.title()` i `person.pwzNumber()` zwracają wpisane wartości. RED (kompilacja nie przejdzie, bo konstruktor ma 5 argumentów) → dodaj pola → GREEN.
2. Znajdź **wszystkie** miejsca konstruujące `new Person(...)` (`grep -rn "new Person(" src/`) i dopisz `null, null` (lub realną wartość jeśli już znana) na końcu wywołania — inaczej build nie skompiluje się.
- DoD: `./mvnw -q compile` przechodzi; `./mvnw -q test -Dtest=PersonTest` zielony.
**T10 — `PwzRegistrySource` (fixture, bez żywego HTTP w teście)**
- Nowy plik: `src/main/java/com/developx/szpitale/ingest/source/PwzRegistrySource.java`.
- Zamiar (sekcja 4.7): metoda `Optional<String> lookupPwz(String fullName)` przyjmuje wynik zapytania do `rejestr.nil.org.pl` jako **wstrzykniętą zależność** (np. interfejs `PwzLookupClient` z metodą `List<String> query(String fullName)`), żeby test nie robił żywego HTTP.
- Fixture: `src/test/resources/fixtures/nil-single-match.json` z jednym wynikiem (`["1234567"]`) i `nil-multiple-matches.json` z dwoma.
- TDD:
1. Test `PwzRegistrySourceTest.lookupPwz_singleMatch_returnsPwzNumber` → mock `PwzLookupClient` zwraca jeden wynik → `lookupPwz` zwraca `Optional.of("1234567")`.
2. Test `PwzRegistrySourceTest.lookupPwz_multipleMatches_returnsEmpty` → mock zwraca 2 wyniki → `lookupPwz` zwraca `Optional.empty()` (wieloznaczność, patrz 4.7 pkt 2 — **nie zgadywać**, który wynik jest właściwy).
3. Test `PwzRegistrySourceTest.lookupPwz_noMatch_returnsEmpty` → mock zwraca pustą listę → `Optional.empty()`.
- DoD: `./mvnw -q test -Dtest=PwzRegistrySourceTest` zielony. Nie implementuj żywego klienta HTTP w tym zadaniu — tylko interfejs `PwzLookupClient` + logika `PwzRegistrySource` na nim.
**T11 — Rekordy `Organization`/`Company` + linki (round-trip)**
- Nowe pliki: `src/main/java/com/developx/szpitale/model/Organization.java`, `Company.java`, `PersonOrganizationLink.java`, `OrganizationLink.java`, `PersonCompanyLink.java` — pola dokładnie jak w sekcji 2 PLAN.md (przykłady JSON), jako rekordy Java z `@JsonProperty` (wzoruj się na `Hospital.java`/`Person.java`).
- Zamiar: `CanonicalDataset` (`src/main/java/com/developx/szpitale/model/CanonicalDataset.java`) dostaje 5 nowych pól-list: `organizations, companies, personOrganizationLinks, organizationLinks, personCompanyLinks` (domyślnie puste listy, nie `null`).
- TDD:
1. Test `OrganizationCompanyRoundTripTest.write_thenRead_preservesOrganizationsAndCompanies` w `src/test/java/com/developx/szpitale/ingest/` (mirror `CanonicalRoundTripTest` z T5): zbuduj `CanonicalDataset` z 1 `Organization`, 1 `Company`, po 1 linku każdego typu; zapisz `CanonicalWriter`, odczytaj `CanonicalReader`, porównaj pola. RED najpierw.
2. Minimalny kod: dodaj rekordy + pola w `CanonicalDataset`, sprawdź że Jackson serializuje/deserializuje bez adnotacji dodatkowych (jak reszta modelu).
- DoD: `./mvnw -q test -Dtest=OrganizationCompanyRoundTripTest` zielony.
**T12 — [NEO4J] `GraphLoader` ładuje `:Organization`/`:Company`**
- Plik prod: `src/main/java/com/developx/szpitale/load/GraphLoader.java` (przeczytaj istniejącą metodę ładującą `Hospital`/`Person`, wzoruj się na jej strukturze).
- Zamiar (sekcja 5.4): dodać metodę analogiczną do istniejącego ładowania węzłów, wykonującą `UNWIND $organizations ... MERGE (:Organization {id})`, `UNWIND $companies ... MERGE (:Company {id})` oraz 3 `MERGE` relacji: `CZLONEK_ORGANIZACJI`, `POWIAZANA_Z`, `POWIAZANY_Z_FIRMA` (dokładne zapytania Cypher w sekcji 5.4 PLAN.md).
- Test integracyjny: `GraphLoaderOrganizationIT` z `@Testcontainers` (wzoruj się na istniejącym wzorcu z T6, jeśli `GraphLoaderIT` już istnieje — skopiuj setup kontenera).
- `load_organizationAndCompany_nodesCreatedWithLinks` → załaduj 1 osobę + 1 organizację + 1 firmę + odpowiednie linki, sprawdź że istnieją węzły `:Organization`, `:Company` i relacje do `:Person`.
- Wymaga Dockera. Bez Dockera → `[~]` + notatka (jak T6/T7).
- DoD: `./mvnw -q test -Dtest=GraphLoaderOrganizationIT` zielony (lub pominięte z notatką).
**T13 — [NEO4J] `GraphValidator` — zapytanie o konflikt interesów**
- Plik prod: `src/main/java/com/developx/szpitale/load/GraphValidator.java`.
- Zamiar (sekcja 5.6): dodać metodę zwracającą wynik zapytania „osoby w organie szpitala z powiązaniem biznesowym" (dokładny Cypher w sekcji 5.6 PLAN.md, blok „konflikt interesów").
- Test integracyjny `GraphValidatorOrganizationIT`: wgraj dataset z 1 osobą mającą `CZLONEK_ORGANU` (szpital) i `POWIAZANY_Z_FIRMA` (firma); asercja: wynik zawiera tę osobę z nazwą firmy.
- Wymaga Dockera. Bez Dockera → `[~]` + notatka.
- DoD: `./mvnw -q test -Dtest=GraphValidatorOrganizationIT` zielony (lub pominięte z notatką).
---
## 12. Sekcja postępu (aktualizuje lokalny agent)
@@ -563,15 +730,30 @@ Po ukończeniu zadania agent zmienia jego status w liście poniżej i dopisuje j
- [x] T1 Deduplicator compile fix
- [x] T2 NameNormalizer testy
- [ ] T3 PartyNormalizer testy
- [x] T3 PartyNormalizer testy
- [x] T4 MarkdownRegistrySource test
- [x] T5 Canonical round-trip
- [~] T6 GraphLoader idempotencja [NEO4J] (skipped: Docker API too old)
- [ ] T7 GraphValidator + CSV [NEO4J]
- [ ] T8 Zielony pełny build
- [~] T6 GraphLoader idempotencja [NEO4J] (skipped: docker-java 3.4.0 in testcontainers 1.20.x hardcodes API v1.32, Daemon requires v1.44+)
- [~] T7 GraphValidator + CSV [NEO4J] (skipped: same docker-api issue as T6)
- [x] T8 Zielony pełny build
- [x] T9 Person: title/pwzNumber
- [x] T10 PwzRegistrySource
- [x] T11 Organization/Company rekordy + round-trip
- [~] T12 GraphLoader: Organization/Company [NEO4J] (skipped: Docker API version mismatch - testcontainers docker-java client v1.32, min 1.44)
- [~] T13 GraphValidator: konflikt interesow [NEO4J] (skipped: Docker API too old — same cause as T6)
**Dziennik (data — zadanie — wynik):**
- (pusto)
- 2026-07-07 — T1 — Deduplicator compile fix + unit tests [checkpoint] (commit 38a93d0)
- 2026-07-07 — T2 — NameNormalizer testy + transliteratePolish, token-strip titles [checkpoint] (commit afe107c)
- 2026-07-07 — T3 — PartyNormalizer testy + KO alias [checkpoint] (commit bc4eaac)
- 2026-07-07 — T4 — MarkdownRegistrySource test + fix isPersonTable check [checkpoint] (commit 7f636a1)
- 2026-07-07 — T5 — CanonicalWriter/Reader round-trip test [checkpoint] (commit b072cb3)
- 2026-07-07 — T6/T8 — skip NEO4J tests (Docker API zbyt stary) + pełny build zielony [checkpoint] (commit 59d27b1)
- 2026-07-08 — PLAN.md scalony z PLAN2.md (Person.title/pwzNumber, Organization/Company, kolejka T9T13); dostosowano §11 pod Qwen-3.6-35B-A3B.
- 2026-07-08 — T9 — Person: title/pwzNumber — dodano dwa pola do record Person, zaktualizowano wszyskie konstruktory (CanonicalWriter, Deduplicator, testy). 7 testow zielonych.
- 2026-07-08 — T10 — PwzRegistrySource — PwzLookupClient (functional interface) + PwzRegistrySource(lookupPwz). 4 testy zielone.
- 2026-07-08 — T11 — Organization/Company entites + round-trip — Organization, Company, PersonOrganizationLink, OrganizationLink, PersonCompanyLink + LinkBasis enum. CanonicalDataset + CanonicalReader zaktualizowane.
- 2026-07-08 — T12 — GraphLoader Organization/Company loading (prod code committed), GraphLoaderOrganizationIT (skipped: Docker API too old, same as T6/T7)
---
+58 -64
View File
@@ -1,84 +1,78 @@
# Hospital Leadership Graph Database
# szpitale-graph
This repository contains the implementation for building a Neo4j graph database of Polish hospitals and their leadership structures.
Graph DB project. Polish hospitals data in Neo4j.
## Overview
Two-layer architecture. Ingest sources → canonical JSON → load into Neo4j.
The project creates a graph database that links:
- **Hospitals** in Poland → **Directors** (`[:DYREKTOR]`)
- **Hospitals** → **Supervisory Boards** (`[:RADNIA]`)
- **Supervisory Board members** → **Political parties** (`[:CZŁONEK_PARTII]`)
- **Supervisory Board members** → **Regional councilors** (`[:RADNY]`) when the same person holds both roles
## Stack
## Implementation
- Java 21
- Maven (use `./mvnw` wrapper)
- Spring Boot 3.4.4 + Spring Shell
- Neo4j 5
- JUnit 5 + Testcontainers
The implementation consists of:
## Key commands
1. **build_hospital_graph.py** - Main Python script that:
- Sets up Neo4j connection and schema
- Processes hospital data (using sample data as real API is not accessible)
- Processes councilor data (using sample data)
- Creates graph nodes and relationships
- Generates summary reports
From `szpitale-graph/`:
2. **PLAN.md** - Detailed execution plan outlining the steps and methodology
## Usage
### Prerequisites
- Docker (to run Neo4j)
- Python 3.6+
- Required packages: requests, beautifulsoup4, neo4j, pandas, python-levenshtein
### Setup
```bash
# Run Neo4j container
docker run -d --name neo4j-test -p 7687:7687 -p 7474:7474 -e NEO4J_AUTH=neo4j/password neo4j:latest
# Create virtual environment and install dependencies
python3 -m venv hospital_graph_env
source hospital_graph_env/bin/activate
pip install requests beautifulsoup4 neo4j pandas python-levenshtein
# Run the script
python build_hospital_graph.py
./mvnw -q compile # compile
./mvnw -q test # all tests
./mvnw -q test -Dtest=ClassName # single test
./mvnw -q verify # full verify
java -jar target/szpitale-graph.jar ingest --voivodeship <voiv> # layer 1
java -jar target/szpitale-graph.jar load --voivodeship <voiv> # layer 2
```
## Data Sources
Neo4j:
1. **Hospital List**:
- Primary: NFZ registry or Ministry of Health (currently using sample data due to API unavailability)
- Fallback: Wikipedia's "Lista szpitali w Polsce"
```bash
docker compose up -d # start Neo4j (UI :7474, Bolt :7687)
docker compose down -v # stop + remove volumes
```
2. **Hospital Staff**:
- Official hospital websites (often under "Zarząd" / "Rada Nadzorcza")
## Architecture
3. **Regional Councilors**:
- Biuletyn Informacji Publicznej (BIP) for each voivodeship
```
ingest → canonical/*.json → load → Neo4j
```
4. **Political Party Affiliation**:
- Same BIP pages or party registries
**Layer 1 (ingest):** Source parsing, normalization, deduplication. No Neo4j dependency.
**Layer 2 (load):** Canonical JSON → UNWIND/MERGE into Neo4j. No network dependency.
## Graph Schema
Canonical output: `canonical/<voivodeship>.json` or `canonical/all.json`.
- **Hospital** node with properties: `name`, `city`, `website`, `nip`
- **Person** node with properties: `name`, `party`, `sourceUrls` (array)
- **Council** node with property: `region`
- Relationships:
- `[:DYREKTOR]` from Hospital to Person
- `[:RADNIA]` from Hospital to Person
- `[:RADNY]` from Person to Council (with `region` property)
- `[:CZŁONEK_PARTII]` from Person to Party (if applicable)
## Source data
## Files Generated
Input markdown files: `research-<voivodeship>.md`. Table format:
1. **scrape_log.json** - Log of all scraping attempts
2. **overlap_report.csv** - Summary report of persons in multiple roles
3. **PLAN.md** - Detailed execution plan
```
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL) |
```
## Validation
Every cell must have an `https://...` URL.
The script includes validation to ensure:
- Every hospital has at least one director relationship
- All person nodes have their source URLs properly recorded
- Graph schema constraints are enforced
## Code layout
`src/main/java/com/developx/szpitale/`
| Package | Purpose |
|---|---|
| `model/` | `CanonicalDataset`, `Hospital`, `Person`, `Affiliation`, `Role`, `Mandate`, `RawHospitalRecord` |
| `model/enums/` | `LegalForm`, `RoleType`, `MandateType`, `ConfidenceLevel`, `RoleStatus`, `AffiliationType`, `OrganType`, `SupervisoryBodyType` |
| `ingest/` | `IngestCommand`, `CanonicalWriter`, `IngestResults` |
| `ingest/source/` | `MarkdownRegistrySource`, `VoivodeshipSource` |
| `ingest/normalize/` | `NameNormalizer`, `PartyNormalizer`, `Deduplicator` |
| `load/` | `LoadCommand`, `CanonicalReader`, `GraphSchema`, `GraphLoader`, `GraphValidator` |
| `SzpitaleGraphApplication.java` | `@SpringBootApplication` entry point |
## 16 voivodeships
Supported regions: all Polish voivodeships. Start with `research-<voiv>.md` files, then run `ingest` + `load`.
## Dev
Test-driven development. TDD cycle: red → green → refactor. Tests mirror main package structure.
Integrations tests use Testcontainers with Neo4j container.
Binary file not shown.
+46 -1
View File
@@ -4,7 +4,7 @@
"description": "Stan wykonania kolejki zadań lokalnego agenta (PLAN2.md §11-13). Zrodlo prawdy przy konflikcie: git log.",
"resumeHint": "Wczytaj ten plik, znajdz pierwszy task o statusie todo/in_progress. Jesli in_progress i drzewo git brudne -> git checkout -- . && git clean -fd, ustaw task na todo, zacznij od zera (PLAN2.md §13.1).",
"createdAt": "2026-07-07",
"updatedAt": "2026-07-07",
"updatedAt": "2026-07-08T23:23:00Z",
"tasks": [
{
"id": "T1",
@@ -77,6 +77,51 @@
"startedAt": null,
"finishedAt": "2026-07-07T00:00:00Z",
"note": "mvn verify green. 2 NEO4J skipped."
},
{
"id": "T9",
"title": "Person: title/pwzNumber",
"tag": null,
"status": "done",
"startedAt": "2026-07-08T23:00:00Z",
"finishedAt": "2026-07-08T23:03:00Z",
"note": "Dodano title i pwzNumber do Person record. Zaktualizowano wszystkie nowicje Person (CanonicalWriter, Deduplicator, testy). 7 testow zielonych."
},
{
"id": "T10",
"title": "PwzRegistrySource",
"tag": null,
"status": "done",
"startedAt": "2026-07-08T23:04:00Z",
"finishedAt": "2026-07-08T23:11:00Z",
"note": "PwzRegistrySource z PwzLookupClient. 4 testow zielonych: singleMatch, multipleMatches (ambiguity), noMatch, nullClient."
},
{
"id": "T11",
"title": "Organization/Company rekordy + round-trip",
"tag": null,
"status": "done",
"startedAt": "2026-07-08T23:12:00Z",
"finishedAt": "2026-07-08T23:23:00Z",
"note": "Organization, Company, PersonOrganizationLink, OrganizationLink, PersonCompanyLink rekordy + LinkBasis enum. CanonicalDataset rozszerzony o 5 pól. Round-trip test zielony."
},
{
"id": "T12",
"title": "GraphLoader: Organization/Company",
"tag": "NEO4J",
"status": "skipped",
"startedAt": "2026-07-08T23:25:00Z",
"finishedAt": "2026-07-08T23:38:00Z",
"note": "Docker API version mismatch: testcontainers docker-java client v1.32, min 1.44. Production code (GraphLoader GraphSchema) committed separately."
},
{
"id": "T13",
"title": "GraphValidator: konflikt interesow",
"tag": "NEO4J",
"status": "skipped",
"startedAt": null,
"finishedAt": "2026-07-08T23:38:00Z",
"note": "Docker API too old (same reason as T12/T6/T7)."
}
]
}
@@ -73,7 +73,7 @@ public class CanonicalWriter {
if (!peopleMap.containsKey(personId)) {
peopleMap.put(personId, new Person(
personId, extractedName, personEntry.displayTitle(),
titles, new ArrayList<>()
titles, new ArrayList<>(), null, null
));
personUrlsMap.put(personId, new LinkedHashSet<>());
}
@@ -122,7 +122,7 @@ public class CanonicalWriter {
if (p != null) {
updatedPeople.add(new Person(
p.id(), p.fullName(), p.displayName(),
p.titles(), new ArrayList<>(entry.getValue())
p.titles(), new ArrayList<>(entry.getValue()), null, null
));
}
}
@@ -131,7 +131,8 @@ public class CanonicalWriter {
CanonicalDataset dataset = new CanonicalDataset(
"1.0", voivodeship, LocalDateTime.now(),
hospitals, people, affiliations, roles, List.of()
hospitals, people, affiliations, roles, List.of(),
List.of(), List.of(), List.of(), List.of(), List.of()
);
Path outputFile = canonicalDir.resolve(voivodeship + ".json");
@@ -49,7 +49,8 @@ public class Deduplicator {
first.fullName(),
first.displayName(),
first.titles(),
new ArrayList<>(allUrls)
new ArrayList<>(allUrls),
null, null
);
}
@@ -0,0 +1,8 @@
package com.developx.szpitale.ingest.source;
import java.util.List;
@FunctionalInterface
public interface PwzLookupClient {
List<String> query(String fullName);
}
@@ -0,0 +1,31 @@
package com.developx.szpitale.ingest.source;
import java.util.List;
import java.util.Optional;
public class PwzRegistrySource {
private final PwzLookupClient client;
public PwzRegistrySource(PwzLookupClient client) {
this.client = client;
}
public Optional<String> lookupPwz(String fullName) {
if (client == null || fullName == null || fullName.isBlank()) {
return Optional.empty();
}
List<String> results = client.query(fullName);
if (results == null || results.isEmpty()) {
return Optional.empty();
}
if (results.size() == 1) {
return Optional.of(results.get(0));
}
return Optional.empty();
}
}
@@ -4,6 +4,7 @@ import com.developx.szpitale.model.CanonicalDataset;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -50,4 +51,10 @@ public class CanonicalReader {
public CanonicalDataset readSingle(Path inputDir, String voivodeship) throws IOException {
return readAll(inputDir, voivodeship).stream().findFirst().orElse(null);
}
public static CanonicalDataset readDataset(File file) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
return mapper.readValue(file, CanonicalDataset.class);
}
}
@@ -30,6 +30,11 @@ public class GraphLoader {
loadRoles(session, dataset.roles(), stats);
loadAffiliations(session, dataset.affiliations(), stats);
loadMandates(session, dataset.mandates(), stats);
loadOrganizations(session, dataset.organizations(), stats);
loadCompanies(session, dataset.companies(), stats);
loadPersonOrganizationLinks(session, dataset.personOrganizationLinks(), stats);
loadOrganizationLinks(session, dataset.organizationLinks(), stats);
loadPersonCompanyLinks(session, dataset.personCompanyLinks(), stats);
}
}
return stats;
@@ -176,6 +181,130 @@ public class GraphLoader {
}
}
private void loadOrganizations(Session session, List<Organization> organizations, LoadStats stats) {
if (organizations.isEmpty()) return;
List<Map<String, Object>> batch = new ArrayList<>();
for (Organization o : organizations) {
batch.add(Map.of(
"id", o.id(),
"name", o.name(),
"krs", o.krs(),
"nip", o.nip(),
"sourceUrls", o.sourceUrls()
));
}
for (int i = 0; i < batch.size(); i += BATCH_SIZE) {
List<Map<String, Object>> subList = batch.subList(i, Math.min(i + BATCH_SIZE, batch.size()));
String cypher = "UNWIND $batch AS o " +
"MERGE (org:Organization {id: o.id}) " +
"SET org += {name: o.name, krs: o.krs, nip: o.nip, sourceUrls: o.sourceUrls}";
session.run(cypher, Map.of("batch", subList));
stats.addOrganizations(subList.size());
}
}
private void loadCompanies(Session session, List<Company> companies, LoadStats stats) {
if (companies.isEmpty()) return;
List<Map<String, Object>> batch = new ArrayList<>();
for (Company c : companies) {
batch.add(Map.of(
"id", c.id(),
"name", c.name(),
"krs", c.krs(),
"nip", c.nip(),
"ceidgId", c.ceidgId(),
"sourceUrls", c.sourceUrls()
));
}
for (int i = 0; i < batch.size(); i += BATCH_SIZE) {
List<Map<String, Object>> subList = batch.subList(i, Math.min(i + BATCH_SIZE, batch.size()));
String cypher = "UNWIND $batch AS c " +
"MERGE (comp:Company {id: c.id}) " +
"SET comp += {name: c.name, krs: c.krs, nip: c.nip, ceidgId: c.ceidgId, sourceUrls: c.sourceUrls}";
session.run(cypher, Map.of("batch", subList));
stats.addCompanies(subList.size());
}
}
private void loadPersonOrganizationLinks(Session session, List<PersonOrganizationLink> links, LoadStats stats) {
if (links.isEmpty()) return;
List<Map<String, Object>> batch = new ArrayList<>();
for (PersonOrganizationLink l : links) {
batch.add(Map.of(
"personId", l.personId(),
"organizationId", l.organizationId(),
"roleLabel", l.roleLabel(),
"basis", l.basis().name(),
"sourceUrls", l.sourceUrls()
));
}
for (int i = 0; i < batch.size(); i += BATCH_SIZE) {
List<Map<String, Object>> subList = batch.subList(i, Math.min(i + BATCH_SIZE, batch.size()));
String cypher = "UNWIND $batch AS l " +
"MATCH (p:Person {id: l.personId}), (org:Organization {id: l.organizationId}) " +
"MERGE (p)-[m:CZLONEK_ORGANIZACJI]->(org) " +
"SET m += {roleLabel: l.roleLabel, basis: l.basis, sourceUrls: l.sourceUrls}";
session.run(cypher, Map.of("batch", subList));
stats.addPersonOrganizationLinks(subList.size());
}
}
private void loadOrganizationLinks(Session session, List<OrganizationLink> links, LoadStats stats) {
if (links.isEmpty()) return;
List<Map<String, Object>> batch = new ArrayList<>();
for (OrganizationLink l : links) {
batch.add(Map.of(
"fromId", l.fromOrganizationId(),
"toId", l.toOrganizationId(),
"basis", l.basis().name(),
"note", l.note(),
"sourceUrls", l.sourceUrls()
));
}
for (int i = 0; i < batch.size(); i += BATCH_SIZE) {
List<Map<String, Object>> subList = batch.subList(i, Math.min(i + BATCH_SIZE, batch.size()));
String cypher = "UNWIND $batch AS l " +
"MATCH (a:Organization {id: l.fromId}), (b:Organization {id: l.toId}) " +
"MERGE (a)-[m:POWIAZANA_Z]->(b) " +
"SET m += {basis: l.basis, note: l.note, sourceUrls: l.sourceUrls}";
session.run(cypher, Map.of("batch", subList));
stats.addOrganizationLinks(subList.size());
}
}
private void loadPersonCompanyLinks(Session session, List<PersonCompanyLink> links, LoadStats stats) {
if (links.isEmpty()) return;
List<Map<String, Object>> batch = new ArrayList<>();
for (PersonCompanyLink l : links) {
batch.add(Map.of(
"personId", l.personId(),
"companyId", l.companyId(),
"roleLabel", l.roleLabel(),
"basis", l.basis().name(),
"sourceUrls", l.sourceUrls()
));
}
for (int i = 0; i < batch.size(); i += BATCH_SIZE) {
List<Map<String, Object>> subList = batch.subList(i, Math.min(i + BATCH_SIZE, batch.size()));
String cypher = "UNWIND $batch AS l " +
"MATCH (p:Person {id: l.personId}), (comp:Company {id: l.companyId}) " +
"MERGE (p)-[m:POWIAZANY_Z_FIRMA]->(comp) " +
"SET m += {roleLabel: l.roleLabel, basis: l.basis, sourceUrls: l.sourceUrls}";
session.run(cypher, Map.of("batch", subList));
stats.addPersonCompanyLinks(subList.size());
}
}
private String roleTypeToRelName(Role r) {
String organ = r.organ().name();
String role = r.roleType().name();
@@ -196,6 +325,11 @@ public class GraphLoader {
public int roleCount;
public int affiliationCount;
public int mandateCount;
public int organizationCount;
public int companyCount;
public int personOrganizationLinkCount;
public int organizationLinkCount;
public int personCompanyLinkCount;
public final List<String> voivodeships = new ArrayList<>();
public void addVoivodeship(String v) { voivodeships.add(v); }
@@ -204,12 +338,18 @@ public class GraphLoader {
public void addRoles(int count) { roleCount += count; }
public void addAffiliations(int count) { affiliationCount += count; }
public void addMandates(int count) { mandateCount += count; }
public void addOrganizations(int count) { organizationCount += count; }
public void addCompanies(int count) { companyCount += count; }
public void addPersonOrganizationLinks(int count) { personOrganizationLinkCount += count; }
public void addOrganizationLinks(int count) { organizationLinkCount += count; }
public void addPersonCompanyLinks(int count) { personCompanyLinkCount += count; }
@Override
public String toString() {
return String.format(
"Load stats - voivodeships: %s, hospitals: %d, people: %d, roles: %d, affiliations: %d, mandates: %d",
String.join(", ", voivodeships), hospitalCount, personCount, roleCount, affiliationCount, mandateCount
"Load stats - voivodeships: %s, hospitals: %d, people: %d, roles: %d, affiliations: %d, mandates: %d, organizations: %d, companies: %d, personOrgLinks: %d, orgLinks: %d, personCompanyLinks: %d",
String.join(", ", voivodeships), hospitalCount, personCount, roleCount, affiliationCount, mandateCount,
organizationCount, companyCount, personOrganizationLinkCount, organizationLinkCount, personCompanyLinkCount
);
}
}
@@ -21,6 +21,8 @@ public class GraphSchema {
session.run("CREATE CONSTRAINT person_id IF NOT EXISTS FOR (p:Person) REQUIRE p.id IS UNIQUE");
session.run("CREATE CONSTRAINT party_name IF NOT EXISTS FOR (x:Party) REQUIRE x.name IS UNIQUE");
session.run("CREATE CONSTRAINT voiv_slug IF NOT EXISTS FOR (v:Voivodeship) REQUIRE v.slug IS UNIQUE");
session.run("CREATE CONSTRAINT org_id IF NOT EXISTS FOR (o:Organization) REQUIRE o.id IS UNIQUE");
session.run("CREATE CONSTRAINT company_id IF NOT EXISTS FOR (c:Company) REQUIRE c.id IS UNIQUE");
session.run("CREATE INDEX person_name IF NOT EXISTS FOR (p:Person) ON (p.fullName)");
session.run("CREATE INDEX govbody_name IF NOT EXISTS FOR (g:GovBody) ON (g.name)");
log.info("Graph constraints and indexes created");
@@ -13,6 +13,33 @@ public record CanonicalDataset(
@JsonProperty("people") List<Person> people,
@JsonProperty("affiliations") List<Affiliation> affiliations,
@JsonProperty("roles") List<Role> roles,
@JsonProperty("mandates") List<Mandate> mandates
@JsonProperty("mandates") List<Mandate> mandates,
@JsonProperty("organizations") List<Organization> organizations,
@JsonProperty("companies") List<Company> companies,
@JsonProperty("personOrganizationLinks") List<PersonOrganizationLink> personOrganizationLinks,
@JsonProperty("organizationLinks") List<OrganizationLink> organizationLinks,
@JsonProperty("personCompanyLinks") List<PersonCompanyLink> personCompanyLinks
) {
public CanonicalDataset(String schemaVersion, String voivodeship, LocalDateTime generatedAt,
List<Hospital> hospitals, List<Person> people,
List<Affiliation> affiliations, List<Role> roles,
List<Mandate> mandates,
List<Organization> organizations, List<Company> companies,
List<PersonOrganizationLink> personOrganizationLinks,
List<OrganizationLink> organizationLinks,
List<PersonCompanyLink> personCompanyLinks) {
this.schemaVersion = schemaVersion;
this.voivodeship = voivodeship;
this.generatedAt = generatedAt;
this.hospitals = hospitals != null ? hospitals : List.of();
this.people = people != null ? people : List.of();
this.affiliations = affiliations != null ? affiliations : List.of();
this.roles = roles != null ? roles : List.of();
this.mandates = mandates != null ? mandates : List.of();
this.organizations = organizations != null ? organizations : List.of();
this.companies = companies != null ? companies : List.of();
this.personOrganizationLinks = personOrganizationLinks != null ? personOrganizationLinks : List.of();
this.organizationLinks = organizationLinks != null ? organizationLinks : List.of();
this.personCompanyLinks = personCompanyLinks != null ? personCompanyLinks : List.of();
}
}
@@ -0,0 +1,15 @@
package com.developx.szpitale.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record Company(
@JsonProperty("id") String id,
@JsonProperty("name") String name,
@JsonProperty("krs") String krs,
@JsonProperty("nip") String nip,
@JsonProperty("ceidgId") String ceidgId,
@JsonProperty("sourceUrls") List<String> sourceUrls
) {
}
@@ -0,0 +1,14 @@
package com.developx.szpitale.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record Organization(
@JsonProperty("id") String id,
@JsonProperty("name") String name,
@JsonProperty("krs") String krs,
@JsonProperty("nip") String nip,
@JsonProperty("sourceUrls") List<String> sourceUrls
) {
}
@@ -0,0 +1,15 @@
package com.developx.szpitale.model;
import com.developx.szpitale.model.enums.LinkBasis;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record OrganizationLink(
@JsonProperty("fromOrganizationId") String fromOrganizationId,
@JsonProperty("toOrganizationId") String toOrganizationId,
@JsonProperty("basis") LinkBasis basis,
@JsonProperty("note") String note,
@JsonProperty("sourceUrls") List<String> sourceUrls
) {
}
@@ -9,6 +9,8 @@ public record Person(
@JsonProperty("fullName") String fullName,
@JsonProperty("displayName") String displayName,
@JsonProperty("titles") List<String> titles,
@JsonProperty("sourceUrls") List<String> sourceUrls
@JsonProperty("sourceUrls") List<String> sourceUrls,
@JsonProperty("title") String title,
@JsonProperty("pwzNumber") String pwzNumber
) {
}
@@ -0,0 +1,15 @@
package com.developx.szpitale.model;
import com.developx.szpitale.model.enums.LinkBasis;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record PersonCompanyLink(
@JsonProperty("personId") String personId,
@JsonProperty("companyId") String companyId,
@JsonProperty("roleLabel") String roleLabel,
@JsonProperty("basis") LinkBasis basis,
@JsonProperty("sourceUrls") List<String> sourceUrls
) {
}
@@ -0,0 +1,15 @@
package com.developx.szpitale.model;
import com.developx.szpitale.model.enums.LinkBasis;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record PersonOrganizationLink(
@JsonProperty("personId") String personId,
@JsonProperty("organizationId") String organizationId,
@JsonProperty("roleLabel") String roleLabel,
@JsonProperty("basis") LinkBasis basis,
@JsonProperty("sourceUrls") List<String> sourceUrls
) {
}
@@ -0,0 +1,7 @@
package com.developx.szpitale.model.enums;
public enum LinkBasis {
KRS,
NIP,
CEIDG
}
@@ -0,0 +1,133 @@
package com.developx.szpitale.ingest;
import com.developx.szpitale.load.CanonicalReader;
import com.developx.szpitale.model.*;
import com.developx.szpitale.model.enums.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class OrganizationCompanyRoundTripTest {
@TempDir
Path tempDir;
@Test
void write_thenRead_preservesOrganizationsAndCompanies() throws Exception {
Organization organization = new Organization(
"org:fundacja-xyz",
"Fundacja XYZ",
"0000123456",
null,
List.of("https://ems.ms.gov.pl/fundacja-xyz")
);
Company company = new Company(
"company:abc-sp-zoo",
"ABC sp. z o.o.",
"0000654321",
"1234567890",
"CEIDG123",
List.of("https://wyszukiwarka-krs.ms.gov.pl/abc")
);
Person person = new Person(
"person:jan-kochanowicz",
"Jan Kochanowicz",
"prof. dr hab. Jan Kochanowicz",
List.of("prof.", "dr hab."),
List.of("https://uskwb.pl/"),
"prof. dr hab.",
null
);
PersonOrganizationLink pol = new PersonOrganizationLink(
"person:jan-kochanowicz",
"org:fundacja-xyz",
"Prezes Zarządu",
LinkBasis.KRS,
List.of("https://ems.ms.gov.pl/fundacja-xyz")
);
PersonCompanyLink pcl = new PersonCompanyLink(
"person:jan-kochanowicz",
"company:abc-sp-zoo",
"Wspólnik",
LinkBasis.KRS,
List.of("https://wyszukiwarka-krs.ms.gov.pl/abc")
);
OrganizationLink orgLink = new OrganizationLink(
"org:fundacja-xyz",
"org:inna-fundacja",
LinkBasis.NIP,
"wspolny adres",
List.of("https://ems.ms.gov.pl/fundacja-xyz")
);
CanonicalDataset dataset = new CanonicalDataset(
"1.0",
"podlaskie",
LocalDateTime.now(),
List.of(),
List.of(person),
List.of(),
List.of(),
List.of(),
List.of(organization),
List.of(company),
List.of(pol),
List.of(orgLink),
List.of(pcl)
);
Path outputFile = tempDir.resolve("test-canonical.json");
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.writeValue(outputFile.toFile(), dataset);
CanonicalDataset readResult = CanonicalReader.readDataset(outputFile.toFile());
assertNotNull(readResult.organizations());
assertNotNull(readResult.companies());
assertNotNull(readResult.personOrganizationLinks());
assertNotNull(readResult.organizationLinks());
assertNotNull(readResult.personCompanyLinks());
assertEquals(1, readResult.organizations().size());
assertEquals("org:fundacja-xyz", readResult.organizations().get(0).id());
assertEquals("Fundacja XYZ", readResult.organizations().get(0).name());
assertEquals("0000123456", readResult.organizations().get(0).krs());
assertEquals(1, readResult.companies().size());
assertEquals("company:abc-sp-zoo", readResult.companies().get(0).id());
assertEquals("ABC sp. z o.o.", readResult.companies().get(0).name());
assertEquals("1234567890", readResult.companies().get(0).nip());
assertEquals("CEIDG123", readResult.companies().get(0).ceidgId());
assertEquals(1, readResult.personOrganizationLinks().size());
assertEquals("person:jan-kochanowicz", readResult.personOrganizationLinks().get(0).personId());
assertEquals("org:fundacja-xyz", readResult.personOrganizationLinks().get(0).organizationId());
assertEquals(LinkBasis.KRS, readResult.personOrganizationLinks().get(0).basis());
assertEquals(1, readResult.organizationLinks().size());
assertEquals("org:fundacja-xyz", readResult.organizationLinks().get(0).fromOrganizationId());
assertEquals("org:inna-fundacja", readResult.organizationLinks().get(0).toOrganizationId());
assertEquals(LinkBasis.NIP, readResult.organizationLinks().get(0).basis());
assertEquals(1, readResult.personCompanyLinks().size());
assertEquals("person:jan-kochanowicz", readResult.personCompanyLinks().get(0).personId());
assertEquals("company:abc-sp-zoo", readResult.personCompanyLinks().get(0).companyId());
assertEquals(LinkBasis.KRS, readResult.personCompanyLinks().get(0).basis());
}
}
@@ -24,14 +24,16 @@ class DeduplicatorTest {
"Jan Kowalski",
"Jan Kowalski",
List.of(),
List.of("https://example.com/1")
List.of("https://example.com/1"),
null, null
);
Person janKowaski = new Person(
"person:jan-kowaski",
"Jan Kowaski",
"Jan Kowaski",
List.of(),
List.of("https://example.com/2")
List.of("https://example.com/2"),
null, null
);
List<Person> input = List.of(janKowalski, janKowaski);
@@ -50,14 +52,16 @@ class DeduplicatorTest {
"Jan Kowalski",
"Jan Kowalski",
List.of(),
List.of("https://example.com/1")
List.of("https://example.com/1"),
null, null
);
Person second = new Person(
"person:jan-kowalski",
"Jan Kowalski",
"Jan Kowalski",
List.of(),
List.of("https://example.com/2")
List.of("https://example.com/2"),
null, null
);
List<Person> result = deduplicator.deduplicate(List.of(first, second));
@@ -0,0 +1,51 @@
package com.developx.szpitale.ingest.source;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
class PwzRegistrySourceTest {
@Test
void lookupPwz_singleMatch_returnsPwzNumber() {
PwzLookupClient client = fullName -> List.of("1234567");
PwzRegistrySource source = new PwzRegistrySource(client);
Optional<String> result = source.lookupPwz("Jan Kowalski");
assertTrue(result.isPresent());
assertEquals("1234567", result.get());
}
@Test
void lookupPwz_multipleMatches_returnsEmpty() {
PwzLookupClient client = fullName -> List.of("1234567", "7654321");
PwzRegistrySource source = new PwzRegistrySource(client);
Optional<String> result = source.lookupPwz("Jan Kowalski");
assertFalse(result.isPresent(), "Multiple matches should return empty (ambiguity)");
}
@Test
void lookupPwz_noMatch_returnsEmpty() {
PwzLookupClient client = fullName -> List.of();
PwzRegistrySource source = new PwzRegistrySource(client);
Optional<String> result = source.lookupPwz("Jan Kowalski");
assertFalse(result.isPresent(), "No match should return empty");
}
@Test
void lookupPwz_nullClient_returnsEmpty() {
PwzRegistrySource source = new PwzRegistrySource(null);
Optional<String> result = source.lookupPwz("Jan Kowalski");
assertFalse(result.isPresent());
}
}
@@ -40,7 +40,8 @@ class CanonicalRoundTripTest {
"Jan Kowalski",
"Dyrektor Naczelny",
List.of("prof."),
List.of("https://uskwb.pl/dyrekcja")
List.of("https://uskwb.pl/dyrekcja"),
null, null
);
Affiliation affiliation = new Affiliation(
@@ -71,7 +72,8 @@ class CanonicalRoundTripTest {
List.of(person),
List.of(affiliation),
List.of(role),
List.of()
List.of(),
List.of(), List.of(), List.of(), List.of(), List.of()
);
Path canonicalDir = tempDir.resolve("canonical");
@@ -0,0 +1,174 @@
package com.developx.szpitale.load;
import com.developx.szpitale.model.*;
import com.developx.szpitale.model.enums.LinkBasis;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
@Testcontainers
class GraphLoaderOrganizationIT {
@Container
static Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:5")
.withEnv("NEO4JLABS_PLUGINS", "['apoc']")
.withEnv("NEO4J_apoc_export_file_enabled", "true")
.withEnv("NEO4J_apoc_import_file_enabled", "true")
.withEnv("NEO4J_apoc_import_file_use__neo4j__config", "true");
static Driver driver;
@BeforeAll
static void setupDriver() {
driver = org.neo4j.driver.GraphDatabase.driver(
neo4j.getBoltUrl(),
AuthTokens.basic("neo4j", "password"));
}
@Test
void load_organizationAndCompany_nodesCreatedWithLinks() {
GraphSchema schema = new GraphSchema(driver);
schema.createConstraints();
Organization org = new Organization(
"org:test-fundacja",
"Test Fundacja",
"0000111222",
null,
List.of("https://example.org/fundacja")
);
Company company = new Company(
"company:test-sp-zoo",
"Test Sp. z o.o.",
"0000333444",
"1234567890",
null,
List.of("https://example.org/company")
);
Person person = new Person(
"person:test-osoba",
"Test Osoba",
"Test Osoba",
List.of(),
List.of("https://example.org/person"),
null, null
);
PersonOrganizationLink pol = new PersonOrganizationLink(
"person:test-osoba",
"org:test-fundacja",
"Czonek Zarzadu",
LinkBasis.KRS,
List.of("https://example.org/link")
);
PersonCompanyLink pcl = new PersonCompanyLink(
"person:test-osoba",
"company:test-sp-zoo",
"Wspolnik",
LinkBasis.NIP,
List.of("https://example.org/link2")
);
OrganizationLink orgLink = new OrganizationLink(
"org:test-fundacja",
"org:inna-fundacja",
LinkBasis.NIP,
"wspolny nip",
List.of("https://example.org/link3")
);
CanonicalDataset dataset = new CanonicalDataset(
"1.0", "test",
java.time.LocalDateTime.now(),
List.of(),
List.of(person),
List.of(), List.of(), List.of(),
List.of(org),
List.of(company),
List.of(pol),
List.of(orgLink),
List.of(pcl)
);
GraphLoader loader = new GraphLoader(driver);
GraphLoader.LoadStats stats = loader.load(List.of(dataset));
assertEquals(1, stats.organizationCount);
assertEquals(1, stats.companyCount);
assertEquals(1, stats.personOrganizationLinkCount);
assertEquals(1, stats.organizationLinkCount);
assertEquals(1, stats.personCompanyLinkCount);
try (Session session = driver.session()) {
var orgResult = session.run(
"MATCH (o:Organization {id: 'org:test-fundacja'}) RETURN o.name AS name").list();
assertFalse(orgResult.isEmpty());
assertEquals("Test Fundacja", orgResult.get(0).get("name").asString());
var compResult = session.run(
"MATCH (c:Company {id: 'company:test-sp-zoo'}) RETURN c.name AS name").list();
assertFalse(compResult.isEmpty());
assertEquals("Test Sp. z o.o.", compResult.get(0).get("name").asString());
var polResult = session.run(
"MATCH (p:Person {id: 'person:test-osoba'})-" +
"[:CZLONEK_ORGANIZACJI]->(o:Organization) " +
"RETURN o.id AS orgId").list();
assertFalse(polResult.isEmpty());
assertEquals("org:test-fundacja", polResult.get(0).get("orgId").asString());
var pclResult = session.run(
"MATCH (p:Person {id: 'person:test-osoba'})-" +
"[:POWIAZANY_Z_FIRMA]->(c:Company) " +
"RETURN c.id AS compId").list();
assertFalse(pclResult.isEmpty());
assertEquals("company:test-sp-zoo", pclResult.get(0).get("compId").asString());
var orgLinkResult = session.run(
"MATCH (a:Organization {id: 'org:test-fundacja'})-" +
"[:POWIAZANA_Z]->(b:Organization) " +
"RETURN b.id AS toId").list();
assertFalse(orgLinkResult.isEmpty());
assertEquals("org:inna-fundacja", orgLinkResult.get(0).get("toId").asString());
}
}
@Test
void schema_constraints_created() {
GraphSchema schema = new GraphSchema(driver);
schema.createConstraints();
try (Session session = driver.session()) {
var result = session.run("SHOW CONSTRAINTS");
int count = 0;
boolean hasOrgConstraint = false, hasCompConstraint = false;
while (result.hasNext()) {
count++;
var record = result.next();
String constraintName = record.get("name").asString();
if (constraintName.contains("org_id")) {
hasOrgConstraint = true;
}
if (constraintName.contains("company_id")) {
hasCompConstraint = true;
}
}
assertTrue(count > 0, "Should have constraints: found " + count);
assertTrue(hasOrgConstraint, "Organization constraint should exist");
assertTrue(hasCompConstraint, "Company constraint should exist");
}
}
}
@@ -0,0 +1,26 @@
package com.developx.szpitale.model;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class PersonTest {
@Test
void person_withTitleAndPwzNumber_fieldsAccessible() {
Person person = new Person(
"person:jan-kochanowicz",
"Jan Kochanowicz",
"prof. dr hab. n. med. Jan Kochanowicz",
List.of("prof.", "dr hab.", "n. med."),
List.of("https://uskwb.pl/dyrekcja-szpitala/"),
"prof. dr hab. n. med.",
"1234567"
);
assertEquals("prof. dr hab. n. med.", person.title());
assertEquals("1234567", person.pwzNumber());
}
}
@@ -0,0 +1 @@
["1234567", "7654321"]
@@ -0,0 +1 @@
["1234567"]
@@ -1,10 +1,13 @@
com/developx/szpitale/ingest/IngestResults.class
com/developx/szpitale/load/CanonicalReader.class
com/developx/szpitale/ingest/source/VoivodeshipSource.class
com/developx/szpitale/model/PersonOrganizationLink.class
com/developx/szpitale/model/enums/AffiliationType.class
com/developx/szpitale/ingest/source/MarkdownRegistrySource.class
com/developx/szpitale/SzpitaleGraphApplication.class
com/developx/szpitale/model/Company.class
com/developx/szpitale/load/GraphValidator.class
com/developx/szpitale/model/Organization.class
com/developx/szpitale/ingest/IngestCommand.class
com/developx/szpitale/ingest/normalize/PartyNormalizer.class
com/developx/szpitale/load/LoadCommand.class
@@ -13,14 +16,19 @@ com/developx/szpitale/ingest/normalize/Deduplicator.class
com/developx/szpitale/load/GraphSchema.class
com/developx/szpitale/model/enums/OrganType.class
com/developx/szpitale/model/RawHospitalRecord$RawMandate.class
com/developx/szpitale/model/OrganizationLink.class
com/developx/szpitale/model/enums/ConfidenceLevel.class
com/developx/szpitale/ingest/source/PwzRegistrySource.class
com/developx/szpitale/ingest/normalize/NameNormalizer.class
com/developx/szpitale/model/RawHospitalRecord$RawPersonEntry.class
com/developx/szpitale/model/enums/SupervisoryBodyType.class
com/developx/szpitale/load/GraphLoader.class
com/developx/szpitale/model/RawHospitalRecord.class
com/developx/szpitale/ingest/CanonicalWriter.class
com/developx/szpitale/model/PersonCompanyLink.class
com/developx/szpitale/load/GraphLoader$LoadStats.class
com/developx/szpitale/model/enums/LinkBasis.class
com/developx/szpitale/model/enums/RoleType.class
com/developx/szpitale/ingest/source/PwzLookupClient.class
com/developx/szpitale/model/enums/LegalForm.class
com/developx/szpitale/model/enums/RoleStatus.class
@@ -6,6 +6,8 @@
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/ingest/normalize/NameNormalizer.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/ingest/normalize/PartyNormalizer.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/ingest/source/MarkdownRegistrySource.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/ingest/source/PwzLookupClient.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/ingest/source/PwzRegistrySource.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/ingest/source/VoivodeshipSource.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/load/CanonicalReader.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/load/GraphLoader.java
@@ -14,14 +16,20 @@
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/load/LoadCommand.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/Affiliation.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/CanonicalDataset.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/Company.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/Hospital.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/Mandate.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/Organization.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/OrganizationLink.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/Person.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/PersonCompanyLink.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/PersonOrganizationLink.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/RawHospitalRecord.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/Role.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/enums/AffiliationType.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/enums/ConfidenceLevel.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/enums/LegalForm.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/enums/LinkBasis.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/enums/MandateType.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/enums/OrganType.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/main/java/com/developx/szpitale/model/enums/RoleStatus.java
@@ -1,6 +1,10 @@
com/developx/szpitale/load/GraphLoaderOrganizationIT.class
com/developx/szpitale/ingest/OrganizationCompanyRoundTripTest.class
com/developx/szpitale/ingest/source/PwzRegistrySourceTest.class
com/developx/szpitale/load/DockerApiVersionTest.class
com/developx/szpitale/ingest/normalize/NameNormalizerTest.class
com/developx/szpitale/ingest/normalize/PartyNormalizerTest.class
com/developx/szpitale/model/PersonTest.class
com/developx/szpitale/ingest/source/MarkdownRegistrySourceTest.class
com/developx/szpitale/ingest/normalize/DeduplicatorTest.class
com/developx/szpitale/load/GraphLoaderIT.class
com/developx/szpitale/load/CanonicalRoundTripTest.class
@@ -1,6 +1,10 @@
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/test/java/com/developx/szpitale/ingest/OrganizationCompanyRoundTripTest.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/test/java/com/developx/szpitale/ingest/normalize/DeduplicatorTest.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/test/java/com/developx/szpitale/ingest/normalize/NameNormalizerTest.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/test/java/com/developx/szpitale/ingest/normalize/PartyNormalizerTest.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/test/java/com/developx/szpitale/ingest/source/MarkdownRegistrySourceTest.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/test/java/com/developx/szpitale/ingest/source/PwzRegistrySourceTest.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/test/java/com/developx/szpitale/load/CanonicalRoundTripTest.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/test/java/com/developx/szpitale/load/GraphLoaderIT.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/test/java/com/developx/szpitale/load/DockerApiVersionTest.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/test/java/com/developx/szpitale/load/GraphLoaderOrganizationIT.java
/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph/src/test/java/com/developx/szpitale/model/PersonTest.java
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
-------------------------------------------------------------------------------
Test set: com.developx.szpitale.ingest.OrganizationCompanyRoundTripTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.168 s -- in com.developx.szpitale.ingest.OrganizationCompanyRoundTripTest
@@ -1,4 +1,4 @@
-------------------------------------------------------------------------------
Test set: com.developx.szpitale.ingest.normalize.DeduplicatorTest
-------------------------------------------------------------------------------
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.006 s -- in com.developx.szpitale.ingest.normalize.DeduplicatorTest
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.005 s -- in com.developx.szpitale.ingest.normalize.DeduplicatorTest
@@ -1,4 +1,4 @@
-------------------------------------------------------------------------------
Test set: com.developx.szpitale.ingest.normalize.NameNormalizerTest
-------------------------------------------------------------------------------
Tests run: 25, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.078 s -- in com.developx.szpitale.ingest.normalize.NameNormalizerTest
Tests run: 25, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.064 s -- in com.developx.szpitale.ingest.normalize.NameNormalizerTest
@@ -1,4 +1,4 @@
-------------------------------------------------------------------------------
Test set: com.developx.szpitale.ingest.normalize.PartyNormalizerTest
-------------------------------------------------------------------------------
Tests run: 15, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.011 s -- in com.developx.szpitale.ingest.normalize.PartyNormalizerTest
Tests run: 15, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.018 s -- in com.developx.szpitale.ingest.normalize.PartyNormalizerTest
@@ -1,4 +1,4 @@
-------------------------------------------------------------------------------
Test set: com.developx.szpitale.ingest.source.MarkdownRegistrySourceTest
-------------------------------------------------------------------------------
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.100 s -- in com.developx.szpitale.ingest.source.MarkdownRegistrySourceTest
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.090 s -- in com.developx.szpitale.ingest.source.MarkdownRegistrySourceTest
@@ -0,0 +1,4 @@
-------------------------------------------------------------------------------
Test set: com.developx.szpitale.ingest.source.PwzRegistrySourceTest
-------------------------------------------------------------------------------
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.005 s -- in com.developx.szpitale.ingest.source.PwzRegistrySourceTest
@@ -1,4 +1,4 @@
-------------------------------------------------------------------------------
Test set: com.developx.szpitale.load.CanonicalRoundTripTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.187 s -- in com.developx.szpitale.load.CanonicalRoundTripTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.024 s -- in com.developx.szpitale.load.CanonicalRoundTripTest
@@ -0,0 +1,23 @@
-------------------------------------------------------------------------------
Test set: com.developx.szpitale.load.DockerApiVersionTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.212 s <<< FAILURE! -- in com.developx.szpitale.load.DockerApiVersionTest
com.developx.szpitale.load.DockerApiVersionTest -- Time elapsed: 0.212 s <<< ERROR!
java.lang.IllegalStateException: Could not find a valid Docker environment. Please see logs and check configuration
at org.testcontainers.dockerclient.DockerClientProviderStrategy.lambda$getFirstValidStrategy$7(DockerClientProviderStrategy.java:274)
at java.base/java.util.Optional.orElseThrow(Optional.java:403)
at org.testcontainers.dockerclient.DockerClientProviderStrategy.getFirstValidStrategy(DockerClientProviderStrategy.java:265)
at org.testcontainers.DockerClientFactory.getOrInitializeStrategy(DockerClientFactory.java:154)
at org.testcontainers.DockerClientFactory.client(DockerClientFactory.java:196)
at org.testcontainers.DockerClientFactory$1.getDockerClient(DockerClientFactory.java:108)
at com.github.dockerjava.api.DockerClientDelegate.authConfig(DockerClientDelegate.java:109)
at org.testcontainers.containers.GenericContainer.start(GenericContainer.java:321)
at org.testcontainers.junit.jupiter.TestcontainersExtension$StoreAdapter.start(TestcontainersExtension.java:276)
at org.testcontainers.junit.jupiter.TestcontainersExtension$StoreAdapter.access$200(TestcontainersExtension.java:263)
at org.testcontainers.junit.jupiter.TestcontainersExtension.lambda$startContainers$4(TestcontainersExtension.java:83)
at org.testcontainers.junit.jupiter.TestcontainersExtension.lambda$startContainers$5(TestcontainersExtension.java:83)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)
at org.testcontainers.junit.jupiter.TestcontainersExtension.startContainers(TestcontainersExtension.java:83)
at org.testcontainers.junit.jupiter.TestcontainersExtension.beforeAll(TestcontainersExtension.java:57)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)
@@ -0,0 +1,23 @@
-------------------------------------------------------------------------------
Test set: com.developx.szpitale.load.GraphLoaderOrganizationIT
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.265 s <<< FAILURE! -- in com.developx.szpitale.load.GraphLoaderOrganizationIT
com.developx.szpitale.load.GraphLoaderOrganizationIT -- Time elapsed: 0.265 s <<< ERROR!
java.lang.IllegalStateException: Could not find a valid Docker environment. Please see logs and check configuration
at org.testcontainers.dockerclient.DockerClientProviderStrategy.lambda$getFirstValidStrategy$7(DockerClientProviderStrategy.java:274)
at java.base/java.util.Optional.orElseThrow(Optional.java:403)
at org.testcontainers.dockerclient.DockerClientProviderStrategy.getFirstValidStrategy(DockerClientProviderStrategy.java:265)
at org.testcontainers.DockerClientFactory.getOrInitializeStrategy(DockerClientFactory.java:154)
at org.testcontainers.DockerClientFactory.client(DockerClientFactory.java:196)
at org.testcontainers.DockerClientFactory$1.getDockerClient(DockerClientFactory.java:108)
at com.github.dockerjava.api.DockerClientDelegate.authConfig(DockerClientDelegate.java:109)
at org.testcontainers.containers.GenericContainer.start(GenericContainer.java:321)
at org.testcontainers.junit.jupiter.TestcontainersExtension$StoreAdapter.start(TestcontainersExtension.java:276)
at org.testcontainers.junit.jupiter.TestcontainersExtension$StoreAdapter.access$200(TestcontainersExtension.java:263)
at org.testcontainers.junit.jupiter.TestcontainersExtension.lambda$null$4(TestcontainersExtension.java:83)
at org.testcontainers.junit.jupiter.TestcontainersExtension.lambda$startContainers$5(TestcontainersExtension.java:83)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)
at org.testcontainers.junit.jupiter.TestcontainersExtension.startContainers(TestcontainersExtension.java:83)
at org.testcontainers.junit.jupiter.TestcontainersExtension.beforeAll(TestcontainersExtension.java:57)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)
@@ -0,0 +1,4 @@
-------------------------------------------------------------------------------
Test set: com.developx.szpitale.model.PersonTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 s -- in com.developx.szpitale.model.PersonTest
@@ -0,0 +1 @@
["1234567", "7654321"]
@@ -0,0 +1 @@
["1234567"]
@@ -1,649 +0,0 @@
{
"cachedGrowthBookFeatures": {
"tengu_mcp_path_scoped_permissions": true,
"tengu_riverbed_lantern": false,
"tengu_flax_grouse": false,
"tengu_plugin_binary_assets": false,
"tengu_claudeai_mcp_connectors": true,
"tengu_turtle_carbon": true,
"tengu_mocha_barista": true,
"tengu_willow_mode": "hint_v2",
"tengu_collage_kaleidoscope": true,
"tengu_lantern_spool": false,
"tengu_penguin_mode_promo": {
"discountPercent": 0,
"endDate": "Feb 16"
},
"tengu_cloth_snorkel": false,
"tengu_quiet_basalt_echo": false,
"tengu_xterm_atlas_reset": true,
"tengu_silk_almanac": false,
"tengu_streaming_tool_execution2": true,
"tengu_willow_refresh_ttl_hours": 0,
"tengu_mcp_subagent_prompt": false,
"tengu_slate_thimble": false,
"tengu_quartz_meadow": false,
"tengu_sage_compass": {},
"tengu_bad_survey_transcript_ask_config": {
"probability": 0
},
"tengu_log_datadog_events": true,
"tengu_classifier_summary_llm_emit": true,
"tengu_swann_brevity": "focused",
"tengu_willow_sentinel_ttl_hours": 1,
"tengu_cowork_chrome_automode_default": false,
"tengu_loud_sugary_rock": false,
"tengu_cedar_lantern": false,
"tengu_umber_petrel": false,
"tengu_slate_harrier": "off",
"tengu_kairos_push_notifications": true,
"tengu_vellum_ash": [],
"tengu_sm_config": {
"minimumMessageTokensToInit": 150000,
"minimumTokensBetweenUpdate": 40000,
"toolCallsBetweenUpdates": 10
},
"tengu_garnet_finch": false,
"tengu_event_watchdog_default_on": true,
"tengu_doorbell_agave": false,
"tengu_fennel_kite_model": "",
"tengu_fennel_kite": false,
"tengu_shining_fractals": false,
"tengu_onyx_plover": {
"enabled": false,
"minHours": 24,
"minSessions": 3,
"remoteEnabled": false
},
"tengu_copper_meadow": false,
"tengu_ccr_bridge_multi_session": false,
"tengu_ochre_hollow": false,
"tengu_kairos_cron_durable": false,
"tengu_reactive_compact_remote": false,
"tengu_kairos_loop_prompt": true,
"tengu_chomp_inflection": true,
"tengu_surreal_dali": true,
"tengu_scarf_coffee": false,
"tengu_sepia_moth": false,
"tengu_amber_wren": {
"targetedRangeNudge": true,
"maxTokens": 25000
},
"tengu_noreread_q7m_velvet": false,
"tengu_marble_whisper2": true,
"tengu_gleaming_fair": false,
"tengu_cinder_plover": "",
"tengu_byte_stream_idle_timeout_ms": 180000,
"tengu_cobalt_plinth_direct": true,
"tengu_vellum_siding": false,
"tengu_crimson_vector": false,
"tengu_fg_left_arrow_agents": true,
"tengu_slate_meridian": false,
"tengu_auto_mode_default_on": false,
"tengu_slate_finch": false,
"tengu_startup_notice": "",
"tengu-fable-off-switch": {
"activated": false
},
"tengu_cork_m4q": true,
"tengu_idle_amber_finch": false,
"tengu_hazel_osprey_floor": 75000,
"tengu_slim_subagent_claudemd": true,
"tengu_plan_artifact": false,
"tengu_marble_sandcastle": false,
"tengu_amber_redwood2": "",
"tengu_auto_notice_once": false,
"tengu_bridge_repl_v2_config": {
"init_retry_max_attempts": 3,
"init_retry_base_delay_ms": 500,
"init_retry_jitter_fraction": 0.25,
"init_retry_max_delay_ms": 4000,
"http_timeout_ms": 10000,
"uuid_dedup_buffer_size": 2000,
"heartbeat_interval_ms": 20000,
"heartbeat_jitter_fraction": 0.1,
"token_refresh_buffer_ms": 600000,
"teardown_archive_timeout_ms": 1500,
"connect_timeout_ms": 15000,
"min_version": "2.1.70",
"should_show_app_upgrade_message": false
},
"tengu_post_compact_survey": false,
"tengu_review_workflow_routing": false,
"tengu_porch_bell_9f": "",
"tengu_workout2": true,
"tengu_harbor": true,
"tengu_compact_cache_prefix": false,
"tengu_amber_flint": true,
"tengu_kestrel_arch": "OFF",
"tengu_pewter_summit": true,
"tengu_jade_anvil_4": false,
"tengu_cedar_plume": false,
"tengu_vellum_lantern": false,
"tengu_timber_lark": "copy_a",
"tengu_bridge_attestation_enforce": false,
"tengu_billiard_aviary": false,
"tengu_amber_lattice": {
"plugins": [
"security-guidance",
"code-review",
"commit-commands",
"code-simplifier",
"hookify",
"feature-dev",
"frontend-design",
"pr-review-toolkit",
"skill-creator",
"plugin-dev",
"agent-sdk-dev",
"mcp-server-dev",
"claude-code-setup",
"claude-md-management",
"playground",
"ralph-loop",
"explanatory-output-style",
"learning-output-style",
"clangd-lsp",
"csharp-lsp",
"gopls-lsp",
"jdtls-lsp",
"kotlin-lsp",
"lua-lsp",
"php-lsp",
"pyright-lsp",
"ruby-lsp",
"rust-analyzer-lsp",
"swift-lsp",
"typescript-lsp"
]
},
"tengu_amber_sextant": true,
"tengu_slate_nexus": false,
"tengu_amber_rokovoko": 0.2,
"tengu_birch_kettle": false,
"tengu_chair_sermon": false,
"tengu_pewter_kestrel": {
"global": 50000,
"Bash": 30000,
"PowerShell": 30000,
"Grep": 20000,
"Snip": 1000,
"StrReplaceBasedEditTool": 30000,
"BashSearchTool": 20000
},
"tengu_skills_dashboard_enabled": false,
"tengu_sepia_cormorant": [],
"tengu_vscode_review_upsell": false,
"tengu-top-of-feed-tip": {
"tip": "",
"color": ""
},
"tengu_marble_whisper": true,
"tengu_ashen_kelp": true,
"tengu_harbor_ledger": [
{
"marketplace": "claude-plugins-official",
"plugin": "discord"
},
{
"marketplace": "claude-plugins-official",
"plugin": "telegram"
},
{
"marketplace": "claude-plugins-official",
"plugin": "fakechat"
},
{
"marketplace": "claude-plugins-official",
"plugin": "imessage"
}
],
"tengu_velvet_moth": 0.2,
"tengu_usage_overage_included_models": [
"Fable",
"Fable 5"
],
"tengu_ccr_bundle_max_bytes": 104857600,
"tengu_slate_ribbon": true,
"tengu_bridge_requires_action_details": true,
"tengu_classifier_summary_heuristic_emit": true,
"tengu_cobalt_wren": false,
"tengu_ultraplan_config": {
"enabled": true
},
"tengu_red_coaster": false,
"tengu_cobalt_lantern": true,
"tengu_quiet_harbor": false,
"tengu_prompt_cache_1h_config": {
"allowlist": [
"repl_main_thread*",
"sdk",
"auto_mode",
"rolling_compact",
"memdir_relevance",
"agent_classifier",
"prompt_suggestion",
"away_summary",
"extract_memories"
]
},
"tengu_malort_pedway": {
"enabled": false,
"pixelValidation": false,
"clipboardPasteMultiline": true,
"screenshotFilter": true,
"mouseAnimation": true,
"hideBeforeAction": true,
"autoTargetDisplay": true,
"coordinateMode": "pixels"
},
"tengu_vscode_onboarding": false,
"tengu_ccr_bundle_seed_enabled": true,
"tengu_blue_coaster": false,
"tengu_harbor_willow": false,
"tengu_destructive_command_warning": false,
"tengu_flint_harbor_prompt": {
"prompt": "You are helping a power user generate an onboarding guide for teammates who are new to Claude Code. The guide will live in the team's onboarding docs and can be pasted into Claude for an interactive walkthrough.\n\nYou're co-authoring this with them — collaborative and helpful, like a teammate who's done this before and is happy to share.\n\n## Usage data (last {{WINDOW_DAYS}} days)\n\nThis was scanned from the guide creator's local Claude Code transcripts:\n\n```json\n{{USAGE_DATA}}\n```\n\n## Your task\n\nBefore anything else — including before thinking through the classification — output exactly this line as your first visible text:\n\n> Looking at how you've used Claude over the last {{WINDOW_DAYS}} days to put together an onboarding guide for teammates new to Claude Code.\n\nThis must come before any extended thinking about session descriptors. The guide creator is staring at a blank screen until you do. Classification is step 2, not step 1.\n\nGenerate the guide immediately, then ask for revisions. Don't wait for answers first — it's easier for the guide creator to edit a concrete draft than answer abstract questions.\n\n1. **Output the acknowledgment line above.** No thinking, no classification, no tool calls before this. One line, then move on.\n\n2. **Derive the work-type breakdown.** Read the `sessionDescriptors` array — each entry describes one session via its title, any linked code reviews (`prNumbers`), and first user message. Classify each session into one of these task types:\n\n - **build_feature** — new functionality, scripts, tools, config/CI/env setup\n - **debug_fix** — investigating and fixing bugs\n - **improve_quality** — refactoring, tests, cleanup, code review\n - **analyze_data** — queries, metrics, number crunching\n - **plan_design** — architecture, approach, strategy, understanding unfamiliar code, design review\n - **prototype** — spikes, POCs, throwaway exploration\n - **write_docs** — PRDs, RFCs, READMEs, design docs, copy/doc review\n\n Categories describe the *type of task*, not the project or domain — a teammate on any project should recognize them. Review sessions belong with whatever's being reviewed: code review is improve_quality, doc review is write_docs, design review is plan_design. Most sessions fit the list; only invent a new category if it's genuinely a different type of task. Pick the top 3-5 with rough percentages. First messages alone are usually enough; titles and code-review links are enrichment. If first messages are uninformative, use tool and MCP counts as a weak hint. If there are ~0 sessions, leave the breakdown as a TODO.\n\n In the rendered guide, display categories with spaces and title case (e.g. \"Build Feature\" not \"build_feature\").\n\n3. **Gather the remaining pieces.** For repos, start with `currentRepo` and check the workspace for sibling repo directories. For MCP server setup, use each entry's `name` (and `urlOrigin` where present) to infer what the server does and how a teammate would get access. Leave the Team Tips and Get Started sections as TODO placeholders — you'll ask for these in Review and fill them in after.\n\n4. **Write the guide to `ONBOARDING.md`** following this template:\n\n```\n{{GUIDE_TEMPLATE}}\n```\n\n Fill in real numbers from the usage data (not placeholders). Use `generatedBy` for the name; if it's missing, omit the name. Ascii bar charts: `█` for filled, `░` for empty, 20 chars wide. Keep the HTML comment instruction at the bottom exactly as shown.\n\n5. **Render the guide in a code block, then close out the first turn.** You're co-authoring this guide with the guide creator — frame the follow-up as collaboration, not corrections.\n\n After the code block, add a `---` horizontal rule and a `**Review**` heading so the guide is visually separated from your questions. Under the heading, number these three questions:\n\n 1. \"I went with '[X]' for the team name — let me know if that sounds right.\" (or if you couldn't tell: \"What's the team name? I'll add it in.\")\n 2. Is there a starter task for someone new to Claude Code? (ticket or doc link — optional)\n 3. Any team tips you'd tell a new teammate that aren't already in CLAUDE.md?\n\n After they answer, update `ONBOARDING.md` with their team name, tips, and starter task. Then close with this exact line (not numbered, not paraphrased):\n\n Saved to `ONBOARDING.md`. Drop it in your team docs and channels — when a new teammate pastes it into Claude Code, they get a guided onboarding tour from there.\n\n Apply any edits they come back with to the file.",
"guideTemplate": "# Welcome to [Team Name]\n\n## How We Use Claude\n\nBased on [name]'s usage over the last [N] days:\n\nWork Type Breakdown:\n [Category 1] [ascii bar] [N]%\n [Category 2] [ascii bar] [N]%\n [Category 3] [ascii bar] [N]%\n ...\n\nTop Skills & Commands:\n [/command] [ascii bar] [N]x/month\n ...\n\nTop MCP Servers:\n [Server] [ascii bar] [N] calls\n ...\n\n## Your Setup Checklist\n\n### Codebases\n- [ ] [repo-name] — [repo url]\n...\n\n### MCP Servers to Activate\n- [ ] [Server] — [what it's for]. [How to get access]\n...\n\n### Skills to Know About\n- [/command] — [what it does, when the team uses it]\n...\n\n## Team Tips\n\n_TODO_\n\n## Get Started\n\n_TODO_\n\n<!-- INSTRUCTION FOR CLAUDE: A new teammate just pasted this guide for how the\nteam uses Claude Code. You're their onboarding buddy — warm, conversational,\nnot lecture-y.\n\nOpen with a warm welcome — include the team name from the title. Then: \"Your\nteammate uses Claude Code for [list all the work types]. Let's get you started.\"\n\nCheck what's already in place against everything under Setup Checklist\n(including skills), using markdown checkboxes — [x] done, [ ] not yet. Lead\nwith what they already have. One sentence per item, all in one message.\n\nTell them you'll help with setup, cover the actionable team tips, then the\nstarter task (if there is one). Offer to start with the first unchecked item,\nget their go-ahead, then work through the rest one by one.\n\nAfter setup, walk them through the remaining sections — offer to help where you\ncan (e.g. link to channels), and just surface the purely informational bits.\n\nDon't invent sections or summaries that aren't in the guide. The stats are the\nguide creator's personal usage data — don't extrapolate them into a \"team\nworkflow\" narrative. -->",
"windowDays": 30
},
"tengu_nimble_amber_prose": false,
"tengu_pewter_lark": "off",
"tengu_lapis_anchor": "off",
"tengu_review_bughunter_config": {
"enabled": false
},
"tengu_steady_lantern": false,
"tengu_good_survey_transcript_ask_config": {
"probability": 0
},
"tengu_snippet_save": false,
"tengu_bridge_min_version": {
"minVersion": "2.1.139"
},
"tengu_bramble_lintel": 7,
"tengu_sparrow_ledger": false,
"tengu_willow_census_ttl_hours": 24,
"tengu_orchid_mantis_v2": false,
"tengu_feature_template": false,
"tengu_cobalt_heron": false,
"tengu_maple_tide": false,
"tengu_tense_pond": false,
"tengu_1p_event_batch_config": {
"scheduledDelayMillis": 10000,
"maxExportBatchSize": 400,
"maxQueueSize": 8192,
"path": "/api/event_logging/v2/batch"
},
"tengu_walrus_canteen": false,
"tengu_retire_chat_relay_artifact_backstop": false,
"tengu_orford_ness": false,
"tengu_flint_harbor_share": false,
"tengu_velvet_harbor": false,
"tengu_anchor_tide": true,
"tengu_otk_slot_v1": false,
"tengu_workflows_enabled": true,
"tengu_mcp_stateless_skip_init": true,
"tengu_osprey_lantern": false,
"tengu_keybinding_customization_release": true,
"tengu_worktree_mode": true,
"tengu_native_cursor": true,
"tengu_agent_list_attach": false,
"tengu_react_vulnerability_warning": false,
"tengu_malformed_tool_use_clean_retry": false,
"tengu_amber_lynx": false,
"tengu_passport_quail": false,
"tengu_aura_sieve": false,
"tengu_sage_compass2": {
"enabled": true
},
"tengu_pewter_ledger": "OFF",
"tengu_ide_rc_auto_enable": false,
"tengu_cobalt_plinth_reader_persist": false,
"tengu_copper_thistle": false,
"tengu_ultraplan_timeout_seconds": 5400,
"tengu_gorse_fathom": false,
"tengu_hazel_osprey": false,
"tengu_amber_anchor": false,
"tengu_negative_interaction_transcript_ask_config": {
"probability": 0
},
"tengu_ccr_v2_send_events_cli": true,
"tengu_quiet_slate_wren": false,
"tengu_cedar_halo": false,
"tengu_amber_prism": false,
"tengu_cedar_sundial": false,
"tengu_slate_kestrel": true,
"tengu_dunwich_bell": false,
"tengu_slate_harbor_experiment": false,
"tengu_mcp_retry_failed_remote": false,
"tengu_cobalt_thicket": false,
"tengu_report_findings_tool": false,
"tengu_chert_bezel": true,
"tengu_olive_hinge": "",
"tengu_shoji_engine": false,
"tengu_plum_vx3": true,
"tengu_system_prompt_global_cache": false,
"tengu_tool_pear": false,
"tengu_lumen_thicket_q7": false,
"tengu_moss_anchor": false,
"tengu_trace_lantern": false,
"tengu_drift_lantern": false,
"tengu_basalt_spur": false,
"claude_code_skills_dashboard_enabled_cli": false,
"tengu_mcp_elicitation": true,
"tengu_alder_compass": false,
"tengu_ccr_bridge": true,
"tengu_classifier_disabled_surfaces": "",
"tengu_tool_search_unsupported_models": [
"claude-3-5-haiku",
"claude-3-haiku"
],
"tengu_copper_bridge": true,
"tengu_hawthorn_steeple": false,
"tengu_shale_finch": false,
"tengu_silk_hinge": false,
"tengu_linen_osprey": false,
"tengu_saffron_anchor": true,
"tengu-model-error-overrides": {},
"tengu_team_discovery": false,
"tengu_moth_copse": false,
"tengu_permission_friction": true,
"tengu_walnut_prism": false,
"tengu_ember_latch": false,
"tengu_mcp_singleton_unwrap": true,
"tengu_slate_siskin": {
"enabled": false,
"timeoutMs": 8000,
"throttleMs": 30000,
"summaryLineThreshold": 5
},
"tengu_mcp_local_oauth_blocked_hosts": {
"hosts": [
"microsoft365.mcp.claude.com",
"gmail.mcp.claude.com",
"gcal.mcp.claude.com"
]
},
"tengu_session_memory": false,
"tengu_md_artifact_styling": false,
"tengu_prism_ledger": false,
"tengu_bridge_poll_interval_ms": 0,
"tengu_harbor_permissions": true,
"tengu_lapis_finch": true,
"tengu_read_dedup_killswitch": false,
"tengu_sotto_voce": true,
"tengu_saffron_lattice": {
"enabled": true,
"planLimitsEndDate": "2026-07-13T07:00:00Z"
},
"tengu_scratch": false,
"tengu_tern_alloy": "copy_a",
"tengu_slate_wren": false,
"tengu_event_sampling_config": {},
"tengu_orchid_trellis": false,
"tengu_slate_ibis": true,
"tengu_marble_anvil": true,
"tengu_file_write_optimization": false,
"tengu_ccr_delta_rehydrate": true,
"tengu_kairos_cron": true,
"tengu_lapis_anchor_user_turn": false,
"tengu_fine_survey_transcript_ask_config": {
"probability": 0
},
"tengu_herring_clock": false,
"tengu_xq7_marble_zephyr": false,
"tengu_tussock_oriole": false,
"tengu_code_diff_cli": true,
"tengu_birthday_hat": false,
"tengu_lichen_compass": false,
"tengu_maple_pier": false,
"tengu_basalt_quill": false,
"tengu_hawthorn_window": 200000,
"tengu_amber_sentinel": true,
"tengu_disable_bypass_permissions_mode": false,
"tengu_silent_harbor": false,
"tengu_slate_moth": false,
"tengu_bg_attach_stall_ms": 5000,
"tengu_vscode_feedback_survey": false,
"tengu_kairos_loop_dynamic": true,
"tengu_cobalt_ridge": true,
"tengu_marble_lark": false,
"tengu_slate_fern": true,
"tengu_prompt_suggestion": true,
"tengu_rewind_first_message": true,
"tengu_russet_linnet": false,
"tengu_c4w_usage_limit_notifications_enabled": true,
"tengu_lilac_loom": null,
"tengu_maple_sundial": false,
"tengu_pewter_brook": true,
"tengu_cobalt_harbor": false,
"tengu_cinder_almanac": true,
"tengu_mcp_normalize_root_combinators": [
"*"
],
"tengu_brick_follow": false,
"tengu_cobalt_plinth_putguard": true,
"tengu_orchid_mantis": false,
"tengu_sedge_lantern_holdback": false,
"tengu_cedar_hollow_7m": {},
"tengu_brass_sled": false,
"tengu_auto_mode_config": {
"enabled": "enabled",
"twoStageClassifier": true,
"sameTurnSiblingContext": true,
"jsonlTranscript": true,
"editRemovalVisibility": true,
"editRemovalCap": 3000,
"outcomeVisibility": true,
"s1SuffixByModel": {
"claude-sonnet-5": "\nStage 1 does NOT apply user intent or ALLOW exceptions — stage 2 will handle those. Judge the action by its full effect — what it runs, sends, publishes, or enables — not its surface form. Block if ANY rule could apply. <block> immediately.",
"claude-sonnet-5[1m]": "\nStage 1 does NOT apply user intent or ALLOW exceptions — stage 2 will handle those. Judge the action by its full effect — what it runs, sends, publishes, or enables — not its surface form. Block if ANY rule could apply. <block> immediately."
}
},
"tengu_prompt_cache_diagnostics": true,
"tengu_basalt_sundial": false,
"tengu_basalt_meadow": true,
"tengu_startup_announcements": [],
"tengu_desktop_upsell": {
"enable_shortcut_tip": false,
"enable_startup_dialog": false
},
"tengu_coral_beacon": false,
"tengu_sedge_lantern": true,
"tengu_compass_dial": true,
"tengu_coral_fern": false,
"tengu_gha_plugin_code_review": false,
"tengu_copper_wren": false,
"tengu_bridge_repl_v2": true,
"tengu_amber_quill": false,
"tengu_frond_boric": {},
"tengu_frame_publish_context": true,
"tengu_cobalt_raccoon": true,
"tengu_gypsum_kite": false,
"tengu_cork_lantern": false,
"tengu_amber_heron": false,
"tengu_loggia_carousel": false,
"tengu_velvet_tide": false,
"tengu_cfc_in_product_permissions": true,
"tengu_amber_moleskin": {
"200000": {
"repl": 0.15,
"sdk": 0.15
},
"1000000": {
"repl": 0.15,
"sdk": 0.05
},
"default": {
"repl": 0.15,
"sdk": 0.15
}
},
"tengu_birch_compass": false,
"tengu_ccr_post_turn_summary": false,
"tengu_sub_nomdrep_q7k": true,
"tengu_mint_lanes": false,
"tengu_penguins_enabled": true,
"tengu_max_version_config": {},
"tengu_crystal_beam": {
"budgetTokens": 0
},
"tengu_quartz_vireo": "",
"tengu_cobalt_plinth": true,
"tengu_crimson_echo": {},
"tengu_plugin_official_mkt_git_fallback": true,
"tengu_bridge_attestation_enforce_config": {
"accept_level": "VERIFIED_BY_GATE",
"accept_statuses": []
},
"tengu_lapis_anchor_budget": 15000000,
"tengu_desktop_upsell_v2": {
"enabled": false
},
"tengu_gouda_loop": true,
"tengu_kairos_input_needed_push": true,
"tengu_canary": {},
"tengu_velvet_ibis": {},
"tengu_fgts": true,
"tengu_amber_lark": false,
"tengu_amber_packet": false,
"tengu_slate_meadow": true,
"tengu_cobalt_compass": true,
"tengu_pewter_finch": false,
"tengu_version_config": {
"minVersion": "1.0.24"
},
"tengu_kairos_github_webhooks": false,
"tengu-off-switch": {
"activated": false
},
"tengu_flint_harbor": true,
"tengu_copper_fox": false,
"tengu_bridge_system_init": true,
"tengu_pewter_lantern": false,
"tengu_feedback_survey_config": {
"minTimeBeforeFeedbackMs": 600000,
"minTimeBetweenFeedbackMs": 43200000,
"minTimeBetweenGlobalFeedbackMs": 43200000,
"minUserTurnsBeforeFeedback": 5,
"minUserTurnsBetweenFeedback": 25,
"hideThanksAfterMs": 3000,
"onForModels": [
"*"
],
"probability": 0
},
"tengu_lapis_thicket": false,
"tengu_grey_step2": {
"enabled": true,
"dialogTitle": "We recommend medium effort for Opus",
"dialogDescription": "Effort determines how long Claude thinks for when completing your task. We recommend medium effort for most tasks to balance speed and intelligence and maximize rate limits. Use ultrathink to trigger high effort when needed."
},
"tengu_tide_elm": "off",
"tengu_harbor_prism": true,
"tengu_bridge_poll_interval_config": {
"poll_interval_ms_not_at_capacity": 2000,
"poll_interval_ms_at_capacity": 600000,
"heartbeat_interval_ms": 0,
"multisession_poll_interval_ms_not_at_capacity": 5000,
"multisession_poll_interval_ms_at_capacity": 60000,
"multisession_poll_interval_ms_partial_capacity": 5000,
"non_exclusive_heartbeat_interval_ms": 180000,
"session_keepalive_interval_ms": 0,
"session_keepalive_interval_v2_ms": 0
},
"tengu_dune_wren": false,
"tengu_fleet_past_sessions": false,
"tengu_plank_river_frost": "user_intent",
"tengu_tangerine_ladder_boost": true,
"tengu_birch_lantern": "off",
"tengu_marbled_teal": false,
"tengu_ultraplan_prompt_identifier": "visual_plan",
"tengu_immediate_model_command": false,
"tengu_miraculo_the_bard": false,
"tengu_ember_trail": "0",
"tengu_sessions_elevated_auth_enforcement": true,
"tengu_velvet_hammer_sonnet_4_5": false,
"tengu_velvet_hammer_falcon": false,
"tengu_long_context_survey_trigger_mode": "",
"tengu_saffron_credits_only_tiers": [
"enterprise"
],
"tengu_slate_quill": true,
"tengu_velvet_hammer_opus_4_7": false,
"tengu_velvet_mallet_opus_4_7": false,
"tengu_velvet_hammer_mythos_5": false,
"tengu_velvet_mallet_opus_4_6": false,
"tengu_loud_sugary_rock2": false,
"tengu_velvet_mallet_falcon": false,
"tengu_velvet_hammer_opus_4_8": false,
"tengu_quill_harbor": "acceptEdits",
"tengu_velvet_hammer": false,
"tengu_velvet_mallet_opus_4_8": false,
"tengu_velvet_static": true,
"tengu_velvet_mallet_haiku": false,
"tengu_velvet_hammer_haiku": false,
"tengu_velvet_mallet_sonnet": false,
"tengu_velvet_mallet": false,
"tengu_soft_slate_nudge": "baseline",
"tengu_velvet_mallet_fable_5": false,
"tengu_velvet_hammer_haiku_4_5": false,
"tengu_long_context_survey_threshold": "",
"tengu_tab_read_sep": false,
"tengu_amber_redwood3": "",
"tengu_long_context_survey_probability": "",
"tengu_long_context_survey_question_variant": "",
"tengu_velvet_hammer_sonnet": false,
"tengu_non_deferrable_builtins": {},
"tengu_velvet_mallet_haiku_4_5": false,
"tengu_velvet_mallet_sonnet_4_5": false,
"tengu_velvet_hammer_sonnet_5": false,
"tengu_chrome_auto_enable": false,
"tengu_velvet_hammer_sonnet_4_6": false,
"tengu_feature_claudified_template": false,
"tengu_basalt_tern": false,
"tengu_windows_credman": false,
"tengu_velvet_hammer_opus_4_6": false,
"tengu_velvet_hammer_fable_5": false,
"tengu_lantern_hearth": "off",
"tengu_c4e_slash_upsell": true,
"tengu_velvet_mallet_opus": false,
"tengu_ax_screen_reader": true,
"tengu_velvet_hammer_opus": false
},
"firstStartTime": "2026-07-07T20:54:53.826Z",
"machineID": "45e86e850454bff833f81aa45c58d5562c139fc43286caa5bcfee391e5cb6ebf",
"opusProMigrationComplete": true,
"sonnet1m45MigrationComplete": true,
"seenNotifications": {},
"migrationVersion": 13,
"userID": "54f1105f4a7f627fba0c70b1763aa06dab8a6fe95939407fbaca0ffdfdff44d8",
"cachedExperimentFeatures": [
"tengu_russet_linnet"
],
"cachedGrowthBookFeaturesAt": 1783457694316,
"projects": {
"/home/kruszewskia/Workspace/Private/Szpitale-graph": {
"allowedTools": [],
"mcpContextUris": [],
"mcpServers": {},
"enabledMcpjsonServers": [],
"disabledMcpjsonServers": [],
"hasTrustDialogAccepted": false,
"projectOnboardingSeenCount": 0,
"hasClaudeMdExternalIncludesApproved": false,
"hasClaudeMdExternalIncludesWarningShown": false,
"hasUnseenTeamArtifacts": false,
"loggedAuthoredArtifactPaths": [
".claude/skills/cavecrew/SKILL.md",
".claude/skills/caveman-commit/SKILL.md",
".claude/skills/caveman-compress/SKILL.md",
".claude/skills/caveman-help/SKILL.md",
".claude/skills/caveman-review/SKILL.md",
".claude/skills/caveman-stats/SKILL.md",
".claude/skills/caveman/SKILL.md"
]
}
}
}
@@ -1 +0,0 @@
2026-07-07T20:54:59.311Z
@@ -1,3 +0,0 @@
{
"firstStartTime": "2026-07-07T20:54:53.826Z"
}
@@ -1 +0,0 @@
{"pid":718201,"sessionId":"3ff18b22-fcf1-49b8-94b0-07f88aa78e18","cwd":"/home/kruszewskia/Workspace/Private/Szpitale-graph/szpitale-graph","startedAt":1783457694314,"procStart":"47261479","version":"2.1.202","peerProtocol":1,"kind":"interactive","entrypoint":"sdk-ts","name":"szpitale-graph-3b","nameSource":"derived"}
@@ -1,649 +0,0 @@
{
"cachedGrowthBookFeatures": {
"tengu_agent_list_attach": false,
"tengu_lantern_spool": false,
"tengu_idle_amber_finch": false,
"tengu_flax_grouse": false,
"tengu_doorbell_agave": false,
"tengu_team_discovery": false,
"tengu_brass_sled": false,
"tengu_cobalt_plinth_putguard": true,
"tengu_orford_ness": false,
"tengu_harbor_prism": true,
"tengu_cobalt_thicket": false,
"tengu_garnet_finch": false,
"tengu_bridge_poll_interval_config": {
"poll_interval_ms_not_at_capacity": 2000,
"poll_interval_ms_at_capacity": 600000,
"heartbeat_interval_ms": 0,
"multisession_poll_interval_ms_not_at_capacity": 5000,
"multisession_poll_interval_ms_at_capacity": 60000,
"multisession_poll_interval_ms_partial_capacity": 5000,
"non_exclusive_heartbeat_interval_ms": 180000,
"session_keepalive_interval_ms": 0,
"session_keepalive_interval_v2_ms": 0
},
"tengu_pewter_finch": false,
"tengu_lapis_thicket": false,
"tengu_scarf_coffee": false,
"tengu_onyx_plover": {
"enabled": false,
"minHours": 24,
"minSessions": 3,
"remoteEnabled": false
},
"tengu_post_compact_survey": false,
"tengu_tern_alloy": "copy_a",
"tengu_byte_stream_idle_timeout_ms": 180000,
"tengu_flint_harbor_prompt": {
"prompt": "You are helping a power user generate an onboarding guide for teammates who are new to Claude Code. The guide will live in the team's onboarding docs and can be pasted into Claude for an interactive walkthrough.\n\nYou're co-authoring this with them — collaborative and helpful, like a teammate who's done this before and is happy to share.\n\n## Usage data (last {{WINDOW_DAYS}} days)\n\nThis was scanned from the guide creator's local Claude Code transcripts:\n\n```json\n{{USAGE_DATA}}\n```\n\n## Your task\n\nBefore anything else — including before thinking through the classification — output exactly this line as your first visible text:\n\n> Looking at how you've used Claude over the last {{WINDOW_DAYS}} days to put together an onboarding guide for teammates new to Claude Code.\n\nThis must come before any extended thinking about session descriptors. The guide creator is staring at a blank screen until you do. Classification is step 2, not step 1.\n\nGenerate the guide immediately, then ask for revisions. Don't wait for answers first — it's easier for the guide creator to edit a concrete draft than answer abstract questions.\n\n1. **Output the acknowledgment line above.** No thinking, no classification, no tool calls before this. One line, then move on.\n\n2. **Derive the work-type breakdown.** Read the `sessionDescriptors` array — each entry describes one session via its title, any linked code reviews (`prNumbers`), and first user message. Classify each session into one of these task types:\n\n - **build_feature** — new functionality, scripts, tools, config/CI/env setup\n - **debug_fix** — investigating and fixing bugs\n - **improve_quality** — refactoring, tests, cleanup, code review\n - **analyze_data** — queries, metrics, number crunching\n - **plan_design** — architecture, approach, strategy, understanding unfamiliar code, design review\n - **prototype** — spikes, POCs, throwaway exploration\n - **write_docs** — PRDs, RFCs, READMEs, design docs, copy/doc review\n\n Categories describe the *type of task*, not the project or domain — a teammate on any project should recognize them. Review sessions belong with whatever's being reviewed: code review is improve_quality, doc review is write_docs, design review is plan_design. Most sessions fit the list; only invent a new category if it's genuinely a different type of task. Pick the top 3-5 with rough percentages. First messages alone are usually enough; titles and code-review links are enrichment. If first messages are uninformative, use tool and MCP counts as a weak hint. If there are ~0 sessions, leave the breakdown as a TODO.\n\n In the rendered guide, display categories with spaces and title case (e.g. \"Build Feature\" not \"build_feature\").\n\n3. **Gather the remaining pieces.** For repos, start with `currentRepo` and check the workspace for sibling repo directories. For MCP server setup, use each entry's `name` (and `urlOrigin` where present) to infer what the server does and how a teammate would get access. Leave the Team Tips and Get Started sections as TODO placeholders — you'll ask for these in Review and fill them in after.\n\n4. **Write the guide to `ONBOARDING.md`** following this template:\n\n```\n{{GUIDE_TEMPLATE}}\n```\n\n Fill in real numbers from the usage data (not placeholders). Use `generatedBy` for the name; if it's missing, omit the name. Ascii bar charts: `█` for filled, `░` for empty, 20 chars wide. Keep the HTML comment instruction at the bottom exactly as shown.\n\n5. **Render the guide in a code block, then close out the first turn.** You're co-authoring this guide with the guide creator — frame the follow-up as collaboration, not corrections.\n\n After the code block, add a `---` horizontal rule and a `**Review**` heading so the guide is visually separated from your questions. Under the heading, number these three questions:\n\n 1. \"I went with '[X]' for the team name — let me know if that sounds right.\" (or if you couldn't tell: \"What's the team name? I'll add it in.\")\n 2. Is there a starter task for someone new to Claude Code? (ticket or doc link — optional)\n 3. Any team tips you'd tell a new teammate that aren't already in CLAUDE.md?\n\n After they answer, update `ONBOARDING.md` with their team name, tips, and starter task. Then close with this exact line (not numbered, not paraphrased):\n\n Saved to `ONBOARDING.md`. Drop it in your team docs and channels — when a new teammate pastes it into Claude Code, they get a guided onboarding tour from there.\n\n Apply any edits they come back with to the file.",
"guideTemplate": "# Welcome to [Team Name]\n\n## How We Use Claude\n\nBased on [name]'s usage over the last [N] days:\n\nWork Type Breakdown:\n [Category 1] [ascii bar] [N]%\n [Category 2] [ascii bar] [N]%\n [Category 3] [ascii bar] [N]%\n ...\n\nTop Skills & Commands:\n [/command] [ascii bar] [N]x/month\n ...\n\nTop MCP Servers:\n [Server] [ascii bar] [N] calls\n ...\n\n## Your Setup Checklist\n\n### Codebases\n- [ ] [repo-name] — [repo url]\n...\n\n### MCP Servers to Activate\n- [ ] [Server] — [what it's for]. [How to get access]\n...\n\n### Skills to Know About\n- [/command] — [what it does, when the team uses it]\n...\n\n## Team Tips\n\n_TODO_\n\n## Get Started\n\n_TODO_\n\n<!-- INSTRUCTION FOR CLAUDE: A new teammate just pasted this guide for how the\nteam uses Claude Code. You're their onboarding buddy — warm, conversational,\nnot lecture-y.\n\nOpen with a warm welcome — include the team name from the title. Then: \"Your\nteammate uses Claude Code for [list all the work types]. Let's get you started.\"\n\nCheck what's already in place against everything under Setup Checklist\n(including skills), using markdown checkboxes — [x] done, [ ] not yet. Lead\nwith what they already have. One sentence per item, all in one message.\n\nTell them you'll help with setup, cover the actionable team tips, then the\nstarter task (if there is one). Offer to start with the first unchecked item,\nget their go-ahead, then work through the rest one by one.\n\nAfter setup, walk them through the remaining sections — offer to help where you\ncan (e.g. link to channels), and just surface the purely informational bits.\n\nDon't invent sections or summaries that aren't in the guide. The stats are the\nguide creator's personal usage data — don't extrapolate them into a \"team\nworkflow\" narrative. -->",
"windowDays": 30
},
"tengu_coral_beacon": false,
"tengu_cedar_plume": false,
"tengu_frame_publish_context": true,
"tengu_review_workflow_routing": false,
"tengu_mcp_normalize_root_combinators": [
"*"
],
"tengu_cobalt_plinth": true,
"tengu_vscode_feedback_survey": false,
"tengu_mcp_subagent_prompt": false,
"tengu_bridge_min_version": {
"minVersion": "2.1.139"
},
"tengu_mint_lanes": false,
"tengu_amber_lynx": false,
"tengu_harbor_willow": false,
"tengu_cedar_hollow_7m": {},
"tengu_amber_rokovoko": 0.2,
"tengu_bramble_lintel": 7,
"tengu_prompt_cache_diagnostics": true,
"tengu_review_bughunter_config": {
"enabled": false
},
"tengu_basalt_meadow": true,
"tengu_ultraplan_config": {
"enabled": true
},
"tengu_malformed_tool_use_clean_retry": false,
"tengu_slim_subagent_claudemd": true,
"tengu_md_artifact_styling": false,
"tengu_anchor_tide": true,
"tengu_quartz_vireo": "",
"tengu_willow_refresh_ttl_hours": 0,
"tengu_version_config": {
"minVersion": "1.0.24"
},
"tengu_scratch": false,
"claude_code_skills_dashboard_enabled_cli": false,
"tengu_bridge_requires_action_details": true,
"tengu_ccr_delta_rehydrate": true,
"tengu_event_sampling_config": {},
"tengu_herring_clock": false,
"tengu_bg_attach_stall_ms": 5000,
"tengu_flint_harbor": true,
"tengu_system_prompt_global_cache": false,
"tengu_mcp_path_scoped_permissions": true,
"tengu_slate_siskin": {
"enabled": false,
"timeoutMs": 8000,
"throttleMs": 30000,
"summaryLineThreshold": 5
},
"tengu_birch_compass": false,
"tengu_marbled_teal": false,
"tengu_fennel_kite": false,
"tengu_1p_event_batch_config": {
"scheduledDelayMillis": 10000,
"maxExportBatchSize": 400,
"maxQueueSize": 8192,
"path": "/api/event_logging/v2/batch"
},
"tengu_gypsum_kite": false,
"tengu_moth_copse": false,
"tengu_ember_trail": "0",
"tengu_slate_finch": false,
"tengu_lapis_anchor_user_turn": false,
"tengu_sparrow_ledger": false,
"tengu_read_dedup_killswitch": false,
"tengu_loggia_carousel": false,
"tengu_pewter_ledger": "OFF",
"tengu_marble_lark": false,
"tengu_kairos_loop_prompt": true,
"tengu_native_cursor": true,
"tengu_passport_quail": false,
"tengu_mcp_retry_failed_remote": false,
"tengu_silk_hinge": false,
"tengu_flint_harbor_share": false,
"tengu_kairos_cron": true,
"tengu_amber_lattice": {
"plugins": [
"security-guidance",
"code-review",
"commit-commands",
"code-simplifier",
"hookify",
"feature-dev",
"frontend-design",
"pr-review-toolkit",
"skill-creator",
"plugin-dev",
"agent-sdk-dev",
"mcp-server-dev",
"claude-code-setup",
"claude-md-management",
"playground",
"ralph-loop",
"explanatory-output-style",
"learning-output-style",
"clangd-lsp",
"csharp-lsp",
"gopls-lsp",
"jdtls-lsp",
"kotlin-lsp",
"lua-lsp",
"php-lsp",
"pyright-lsp",
"ruby-lsp",
"rust-analyzer-lsp",
"swift-lsp",
"typescript-lsp"
]
},
"tengu_orchid_mantis_v2": false,
"tengu_hazel_osprey": false,
"tengu_willow_census_ttl_hours": 24,
"tengu_cedar_halo": false,
"tengu_kairos_input_needed_push": true,
"tengu_willow_mode": "hint_v2",
"tengu_prism_ledger": false,
"tengu_quiet_slate_wren": false,
"tengu_classifier_summary_llm_emit": true,
"tengu_ccr_bridge": true,
"tengu_amber_lark": false,
"tengu_workout2": true,
"tengu_billiard_aviary": false,
"tengu-model-error-overrides": {},
"tengu_shining_fractals": false,
"tengu_cobalt_lantern": true,
"tengu_worktree_mode": true,
"tengu_blue_coaster": false,
"tengu_miraculo_the_bard": false,
"tengu_crimson_vector": false,
"tengu_copper_meadow": false,
"tengu_cobalt_plinth_reader_persist": false,
"tengu_marble_anvil": true,
"tengu_velvet_harbor": false,
"tengu_kestrel_arch": "OFF",
"tengu_drift_lantern": false,
"tengu_startup_announcements": [],
"tengu_steady_lantern": false,
"tengu_cedar_lantern": false,
"tengu_quartz_meadow": false,
"tengu_hawthorn_window": 200000,
"tengu_maple_tide": false,
"tengu_cloth_snorkel": false,
"tengu_sessions_elevated_auth_enforcement": true,
"tengu_turtle_carbon": true,
"tengu_coral_fern": false,
"tengu_max_version_config": {},
"tengu_session_memory": false,
"tengu_permission_friction": true,
"tengu_plugin_official_mkt_git_fallback": true,
"tengu_lilac_loom": null,
"tengu_cobalt_plinth_direct": true,
"tengu_birch_lantern": "banner",
"tengu_kairos_cron_durable": false,
"tengu_ultraplan_timeout_seconds": 5400,
"tengu_negative_interaction_transcript_ask_config": {
"probability": 0
},
"tengu_tussock_oriole": false,
"tengu_dunwich_bell": false,
"tengu_cinder_almanac": true,
"tengu_penguin_mode_promo": {
"discountPercent": 0,
"endDate": "Feb 16"
},
"tengu_kairos_github_webhooks": false,
"tengu_code_diff_cli": true,
"tengu_surreal_dali": true,
"tengu_orchid_trellis": false,
"tengu_slate_nexus": false,
"tengu_copper_bridge": true,
"tengu_tangerine_ladder_boost": true,
"tengu_desktop_upsell_v2": {
"enabled": false
},
"tengu_slate_harrier": "off",
"tengu_slate_fern": true,
"tengu_sepia_cormorant": [],
"tengu_tool_pear": false,
"tengu_ccr_bridge_multi_session": false,
"tengu_claudeai_mcp_connectors": true,
"tengu_silk_almanac": false,
"tengu_frond_boric": {},
"tengu_kairos_push_notifications": true,
"tengu_vscode_review_upsell": false,
"tengu_fennel_kite_model": "",
"tengu_disable_bypass_permissions_mode": false,
"tengu_cinder_plover": "",
"tengu_compass_dial": true,
"tengu_ember_latch": false,
"tengu_marble_sandcastle": false,
"tengu_amber_anchor": false,
"tengu_classifier_disabled_surfaces": "",
"tengu_pewter_lantern": false,
"tengu_harbor_ledger": [
{
"marketplace": "claude-plugins-official",
"plugin": "discord"
},
{
"marketplace": "claude-plugins-official",
"plugin": "telegram"
},
{
"marketplace": "claude-plugins-official",
"plugin": "fakechat"
},
{
"marketplace": "claude-plugins-official",
"plugin": "imessage"
}
],
"tengu_loud_sugary_rock": false,
"tengu_pewter_lark": "off",
"tengu_amber_sextant": true,
"tengu_xq7_marble_zephyr": false,
"tengu_amber_redwood2": "",
"tengu_copper_thistle": false,
"tengu_kairos_loop_dynamic": true,
"tengu_walrus_canteen": false,
"tengu_vellum_ash": [],
"tengu_chomp_inflection": true,
"tengu_sage_compass": {},
"tengu_silent_harbor": false,
"tengu_fleet_past_sessions": false,
"tengu_saffron_lattice": {
"enabled": true,
"planLimitsEndDate": "2026-07-13T07:00:00Z"
},
"tengu_willow_sentinel_ttl_hours": 1,
"tengu_slate_meadow": true,
"tengu_bad_survey_transcript_ask_config": {
"probability": 0
},
"tengu_copper_wren": false,
"tengu_crystal_beam": {
"budgetTokens": 0
},
"tengu_cobalt_ridge": true,
"tengu_sotto_voce": true,
"tengu_swann_brevity": "focused",
"tengu_brick_follow": false,
"tengu_nimble_amber_prose": false,
"tengu_copper_fox": false,
"tengu_aura_sieve": false,
"tengu_amber_quill": false,
"tengu_fg_left_arrow_agents": true,
"tengu_marble_whisper": true,
"tengu_slate_harbor_experiment": false,
"tengu_ccr_v2_send_events_cli": true,
"tengu_riverbed_lantern": false,
"tengu_velvet_moth": 0.2,
"tengu-top-of-feed-tip": {
"tip": "",
"color": ""
},
"tengu_birch_kettle": false,
"tengu_slate_moth": false,
"tengu_ide_rc_auto_enable": false,
"tengu_slate_thimble": false,
"tengu_reactive_compact_remote": false,
"tengu_quiet_basalt_echo": false,
"tengu_retire_chat_relay_artifact_backstop": false,
"tengu_basalt_sundial": false,
"tengu_good_survey_transcript_ask_config": {
"probability": 0
},
"tengu_gorse_fathom": false,
"tengu_cowork_chrome_automode_default": false,
"tengu_bridge_repl_v2_config": {
"init_retry_max_attempts": 3,
"init_retry_base_delay_ms": 500,
"init_retry_jitter_fraction": 0.25,
"init_retry_max_delay_ms": 4000,
"http_timeout_ms": 10000,
"uuid_dedup_buffer_size": 2000,
"heartbeat_interval_ms": 20000,
"heartbeat_jitter_fraction": 0.1,
"token_refresh_buffer_ms": 600000,
"teardown_archive_timeout_ms": 1500,
"connect_timeout_ms": 15000,
"min_version": "2.1.70",
"should_show_app_upgrade_message": false
},
"tengu_prompt_suggestion": true,
"tengu_ccr_bundle_seed_enabled": true,
"tengu_lichen_compass": false,
"tengu_bridge_attestation_enforce": false,
"tengu_harbor_permissions": true,
"tengu_tool_search_unsupported_models": [
"claude-3-5-haiku",
"claude-3-haiku"
],
"tengu_orchid_mantis": false,
"tengu_basalt_quill": false,
"tengu_streaming_tool_execution2": true,
"tengu_plum_vx3": true,
"tengu_slate_ibis": true,
"tengu_amber_moleskin": {
"200000": {
"repl": 0.15,
"sdk": 0.15
},
"1000000": {
"repl": 0.15,
"sdk": 0.05
},
"default": {
"repl": 0.15,
"sdk": 0.15
}
},
"tengu_plank_river_frost": "user_intent",
"tengu_feedback_survey_config": {
"minTimeBeforeFeedbackMs": 600000,
"minTimeBetweenFeedbackMs": 43200000,
"minTimeBetweenGlobalFeedbackMs": 43200000,
"minUserTurnsBeforeFeedback": 5,
"minUserTurnsBetweenFeedback": 25,
"hideThanksAfterMs": 3000,
"onForModels": [
"*"
],
"probability": 0
},
"tengu_hazel_osprey_floor": 75000,
"tengu_sub_nomdrep_q7k": true,
"tengu_cobalt_heron": false,
"tengu_feature_template": false,
"tengu_bridge_attestation_enforce_config": {
"accept_level": "VERIFIED_BY_GATE",
"accept_statuses": []
},
"tengu_gleaming_fair": false,
"tengu_maple_pier": false,
"tengu_porch_bell_9f": "",
"tengu_cobalt_raccoon": true,
"tengu_plugin_binary_assets": false,
"tengu_grey_step2": {
"enabled": true,
"dialogTitle": "We recommend medium effort for Opus",
"dialogDescription": "Effort determines how long Claude thinks for when completing your task. We recommend medium effort for most tasks to balance speed and intelligence and maximize rate limits. Use ultrathink to trigger high effort when needed."
},
"tengu_file_write_optimization": false,
"tengu_amber_sentinel": true,
"tengu_moss_anchor": false,
"tengu_cork_lantern": false,
"tengu_slate_meridian": false,
"tengu_fine_survey_transcript_ask_config": {
"probability": 0
},
"tengu_plan_artifact": false,
"tengu_report_findings_tool": false,
"tengu_cfc_in_product_permissions": true,
"tengu_snippet_save": false,
"tengu_marble_whisper2": true,
"tengu_keybinding_customization_release": true,
"tengu-off-switch": {
"activated": false
},
"tengu_jade_anvil_4": false,
"tengu_russet_linnet": false,
"tengu_slate_kestrel": true,
"tengu_ccr_post_turn_summary": false,
"tengu-fable-off-switch": {
"activated": false
},
"tengu_event_watchdog_default_on": true,
"tengu_hawthorn_steeple": false,
"tengu_penguins_enabled": true,
"tengu_gouda_loop": true,
"tengu_timber_lark": "copy_a",
"tengu_mcp_elicitation": true,
"tengu_collage_kaleidoscope": true,
"tengu_bridge_repl_v2": true,
"tengu_sage_compass2": {
"enabled": true
},
"tengu_amber_packet": false,
"tengu_canary": {},
"tengu_skills_dashboard_enabled": false,
"tengu_mcp_stateless_skip_init": true,
"tengu_fgts": true,
"tengu_cork_m4q": true,
"tengu_gha_plugin_code_review": false,
"tengu_pewter_brook": true,
"tengu_auto_mode_config": {
"enabled": "enabled",
"twoStageClassifier": true,
"sameTurnSiblingContext": true,
"jsonlTranscript": true,
"editRemovalVisibility": true,
"editRemovalCap": 3000,
"outcomeVisibility": true,
"s1SuffixByModel": {
"claude-sonnet-5": "\nStage 1 does NOT apply user intent or ALLOW exceptions — stage 2 will handle those. Judge the action by its full effect — what it runs, sends, publishes, or enables — not its surface form. Block if ANY rule could apply. <block> immediately.",
"claude-sonnet-5[1m]": "\nStage 1 does NOT apply user intent or ALLOW exceptions — stage 2 will handle those. Judge the action by its full effect — what it runs, sends, publishes, or enables — not its surface form. Block if ANY rule could apply. <block> immediately."
}
},
"tengu_vellum_lantern": false,
"tengu_mcp_local_oauth_blocked_hosts": {
"hosts": [
"microsoft365.mcp.claude.com",
"gmail.mcp.claude.com",
"gcal.mcp.claude.com"
]
},
"tengu_bridge_poll_interval_ms": 0,
"tengu_velvet_tide": false,
"tengu_lapis_anchor": "off",
"tengu_osprey_lantern": false,
"tengu_workflows_enabled": true,
"tengu_cobalt_compass": true,
"tengu_trace_lantern": false,
"tengu_log_datadog_events": true,
"tengu_birthday_hat": false,
"tengu_chair_sermon": false,
"tengu_umber_petrel": false,
"tengu_tense_pond": false,
"tengu_ochre_hollow": false,
"tengu_desktop_upsell": {
"enable_shortcut_tip": false,
"enable_startup_dialog": false
},
"tengu_compact_cache_prefix": false,
"tengu_sm_config": {
"minimumMessageTokensToInit": 150000,
"minimumTokensBetweenUpdate": 40000,
"toolCallsBetweenUpdates": 10
},
"tengu_sedge_lantern_holdback": false,
"tengu_linen_osprey": false,
"tengu_mcp_singleton_unwrap": true,
"tengu_lumen_thicket_q7": false,
"tengu_amber_prism": false,
"tengu_destructive_command_warning": false,
"tengu_alder_compass": false,
"tengu_lapis_finch": true,
"tengu_basalt_spur": false,
"tengu_react_vulnerability_warning": false,
"tengu_ashen_kelp": true,
"tengu_red_coaster": false,
"tengu_quiet_harbor": false,
"tengu_tide_elm": "off",
"tengu_ultraplan_prompt_identifier": "visual_plan",
"tengu_cobalt_wren": false,
"tengu_rewind_first_message": true,
"tengu_vellum_siding": false,
"tengu_amber_heron": false,
"tengu_dune_wren": false,
"tengu_shale_finch": false,
"tengu_pewter_kestrel": {
"global": 50000,
"Bash": 30000,
"PowerShell": 30000,
"Grep": 20000,
"Snip": 1000,
"StrReplaceBasedEditTool": 30000,
"BashSearchTool": 20000
},
"tengu_auto_mode_default_on": false,
"tengu_crimson_echo": {},
"tengu_slate_ribbon": true,
"tengu_amber_flint": true,
"tengu_slate_wren": false,
"tengu_xterm_atlas_reset": true,
"tengu_mocha_barista": true,
"tengu_prompt_cache_1h_config": {
"allowlist": [
"repl_main_thread*",
"sdk",
"auto_mode",
"rolling_compact",
"memdir_relevance",
"agent_classifier",
"prompt_suggestion",
"away_summary",
"extract_memories"
]
},
"tengu_usage_overage_included_models": [
"Fable",
"Fable 5"
],
"tengu_ccr_bundle_max_bytes": 104857600,
"tengu_pewter_summit": true,
"tengu_chert_bezel": true,
"tengu_shoji_engine": false,
"tengu_otk_slot_v1": false,
"tengu_classifier_summary_heuristic_emit": true,
"tengu_immediate_model_command": false,
"tengu_walnut_prism": false,
"tengu_lapis_anchor_budget": 15000000,
"tengu_amber_wren": {
"targetedRangeNudge": true,
"maxTokens": 25000
},
"tengu_cedar_sundial": false,
"tengu_saffron_anchor": true,
"tengu_malort_pedway": {
"enabled": false,
"pixelValidation": false,
"clipboardPasteMultiline": true,
"screenshotFilter": true,
"mouseAnimation": true,
"hideBeforeAction": true,
"autoTargetDisplay": true,
"coordinateMode": "pixels"
},
"tengu_olive_hinge": "",
"tengu_startup_notice": "",
"tengu_sedge_lantern": true,
"tengu_sepia_moth": false,
"tengu_vscode_onboarding": false,
"tengu_noreread_q7m_velvet": false,
"tengu_c4w_usage_limit_notifications_enabled": true,
"tengu_cobalt_harbor": false,
"tengu_velvet_ibis": {},
"tengu_auto_notice_once": false,
"tengu_maple_sundial": false,
"tengu_bridge_system_init": true,
"tengu_harbor": true,
"tengu_feature_claudified_template": false,
"tengu_loud_sugary_rock2": false,
"tengu_velvet_mallet_sonnet_4_5": false,
"tengu_velvet_hammer_opus_4_7": false,
"tengu_lantern_hearth": "off",
"tengu_amber_redwood3": "",
"tengu_velvet_mallet_opus_4_7": false,
"tengu_velvet_hammer_opus": false,
"tengu_velvet_hammer_sonnet": false,
"tengu_windows_credman": false,
"tengu_velvet_mallet_falcon": false,
"tengu_velvet_hammer": false,
"tengu_saffron_credits_only_tiers": [
"enterprise"
],
"tengu_velvet_hammer_fable_5": false,
"tengu_velvet_mallet_opus_4_6": false,
"tengu_velvet_mallet_haiku_4_5": false,
"tengu_velvet_hammer_sonnet_5": false,
"tengu_long_context_survey_threshold": "",
"tengu_ax_screen_reader": true,
"tengu_basalt_tern": false,
"tengu_long_context_survey_probability": "",
"tengu_velvet_mallet_sonnet": false,
"tengu_velvet_hammer_mythos_5": false,
"tengu_long_context_survey_trigger_mode": "",
"tengu_velvet_hammer_haiku_4_5": false,
"tengu_velvet_mallet_opus": false,
"tengu_chrome_auto_enable": false,
"tengu_non_deferrable_builtins": {},
"tengu_velvet_mallet_fable_5": false,
"tengu_velvet_static": true,
"tengu_quill_harbor": "acceptEdits",
"tengu_velvet_hammer_opus_4_6": false,
"tengu_velvet_hammer_falcon": false,
"tengu_velvet_hammer_opus_4_8": false,
"tengu_soft_slate_nudge": "baseline",
"tengu_c4e_slash_upsell": true,
"tengu_velvet_mallet": false,
"tengu_tab_read_sep": false,
"tengu_velvet_hammer_sonnet_4_5": false,
"tengu_velvet_mallet_haiku": false,
"tengu_long_context_survey_question_variant": "",
"tengu_velvet_hammer_haiku": false,
"tengu_velvet_hammer_sonnet_4_6": false,
"tengu_velvet_mallet_opus_4_8": false,
"tengu_slate_quill": true
},
"firstStartTime": "2026-07-07T21:07:55.988Z",
"machineID": "8286e7c3c8be240dd7563b772b94d91bec260294b4c3c9b6a147c0fad853d6b6",
"opusProMigrationComplete": true,
"sonnet1m45MigrationComplete": true,
"seenNotifications": {},
"migrationVersion": 13,
"userID": "c5874f5dce270856b661111baea070fca33b919a56303632d3bb60e5a1a7f5b9",
"cachedExperimentFeatures": [
"tengu_birch_lantern"
],
"cachedGrowthBookFeaturesAt": 1783458476499,
"projects": {
"/home/kruszewskia/Workspace/Private/Szpitale-graph": {
"allowedTools": [],
"mcpContextUris": [],
"mcpServers": {},
"enabledMcpjsonServers": [],
"disabledMcpjsonServers": [],
"hasTrustDialogAccepted": false,
"projectOnboardingSeenCount": 0,
"hasClaudeMdExternalIncludesApproved": false,
"hasClaudeMdExternalIncludesWarningShown": false,
"hasUnseenTeamArtifacts": false,
"loggedAuthoredArtifactPaths": [
".claude/skills/cavecrew/SKILL.md",
".claude/skills/caveman-commit/SKILL.md",
".claude/skills/caveman-compress/SKILL.md",
".claude/skills/caveman-help/SKILL.md",
".claude/skills/caveman-review/SKILL.md",
".claude/skills/caveman-stats/SKILL.md",
".claude/skills/caveman/SKILL.md"
]
}
}
}
@@ -1 +0,0 @@
2026-07-07T21:08:01.495Z
@@ -1,3 +0,0 @@
{
"firstStartTime": "2026-07-07T21:07:55.988Z"
}