# Etap 1: pierwszy ingest Nostr → Neo4j

Dokument pokrywa minimum potrzebne, żeby załadować do grafu **realne eventy Nostr**. Zakres celowo węższy niż docelowa architektura z `01.md` — admin MCP, client MCP, `:Config:*` i podział ról zostawiamy na później.

## Cel etapu

Jeden proces Python, który:

1. łączy się z kilkoma relays,
2. czyta eventy wg prostego filtra (kindy 0/1/3/7),
3. mapuje je do grafu wg reguł z `01.md` sekcja 4,
4. zapisuje idempotentnie do Neo4j.

Po zakończeniu w bazie są realne węzły `Event` / `Pubkey` / `Tag` / `TagElement` / `Relay` / `Addressable` i relacje między nimi.

## Uproszczenia względem 01.md (świadome, do zdjęcia w kolejnych etapach)

- **Konfiguracja zahardkodowana** w pythonie / prostym YAML — bez `:Config:*` w grafie.
- **Bez admin MCP i client MCP** — tylko ingest.
- **Jeden Neo4j user** zamiast trzech ról.
- **Bez zewnętrznej kontroli procesu** — uruchamiasz ręcznie, przerywasz Ctrl-C.

## Co trzeba przygotować w Neo4j

Constrainty potrzebne do idempotentnego `MERGE`:

```cypher
CREATE CONSTRAINT event_id         IF NOT EXISTS FOR (e:Event)       REQUIRE e.id    IS UNIQUE;
CREATE CONSTRAINT pubkey_hex       IF NOT EXISTS FOR (p:Pubkey)      REQUIRE p.hex   IS UNIQUE;
CREATE CONSTRAINT relay_url        IF NOT EXISTS FOR (r:Relay)       REQUIRE r.url   IS UNIQUE;
CREATE CONSTRAINT addressable_val  IF NOT EXISTS FOR (a:Addressable) REQUIRE a.value IS UNIQUE;
```

`Tag` i `TagElement` są per-event (nie współdzielone między eventami) — nie mają unique constraintu.

## Mapper: event → graf

Pełne reguły: `01.md` sekcja 4. Zasady wbite w kod mappera:

- Wszystko przez `MERGE` na kluczach z constraintów → ponowne przetworzenie eventu = no-op.
- `Tag` i `TagElement` tworzymy **nowe** dla każdego eventu; kolejność przez `position_in_event` i `position`.
- Dla tagów `e` / `p` / `a` / `r` tworzymy `REFERS_TO` do target-node'a (`a` → parsowanie `kind:pubkey:d`).
- `Event.content` zapisujemy surowo (także dla kind 0 — bez parsowania JSON-a profilu).
- Cały mapping per event = jeden Cypher z `UNWIND` po tagach w jednej transakcji.

## Klient Nostr

- Biblioteka: **pynostr** (alternatywa: `nostr-sdk` Python bindings — decyzja przy pierwszym spike'u).
- Dwa tryby:
  - **Backfill**: pobierz N ostatnich eventów wg filtra, zapisz, zamknij.
  - **Live**: trzymaj subskrypcję, zapisuj na bieżąco.
- Deduplikacja po stronie ingestu zbędna — `MERGE` w Neo4j załatwia idempotencję.

## Seed konfiguracji (hardcode na start)

```python
RELAYS = [
    "wss://relay.damus.io",
    "wss://nos.lol",
    "wss://relay.nostr.band",
]

FILTER = {
    "kinds": [0, 1, 3, 7],   # profiles, notes, contacts, reactions
    "limit": 500,            # dla backfillu
}
```

## Do rozstrzygnięcia przed kodowaniem

1. **pynostr vs nostr-sdk** — który faktycznie bierzemy? Decyzja po krótkim spike'u na obu.
2. **Backfill → live**: jeden proces robi backfill i wchodzi w live, czy dwa tryby wybierane flagą CLI?
3. **Batching zapisów**: per-event (prosto) vs batch po N eventów (szybciej, trudniej z błędami). Proponuję per-event na start.
4. **Credentials Neo4j**: reuse admina na etap 1, czy od razu osobny user `ingest`?
5. **Obsługa błędów mappingu**: log + skip (proponowane) vs crash całego procesu.

## Definition of done

- Neo4j stoi (Docker), constrainty założone.
- `python -m ingest.main --backfill` ładuje N eventów z seed-relays.
- W Neo4j Browser widać węzły `Event`/`Pubkey`/`Tag`/`TagElement`/`Relay` i relacje `AUTHORED_BY` / `HAS_TAG` / `HAS_ELEMENT` / `REFERS_TO` / `SEEN_ON`.
- Ponowne uruchomienie backfillu nie duplikuje węzłów.
