- Omorganiser docs/: konsepter, features, infra og proposals i egne mapper - Ny docs/erfaringer/ med lærdommer fra chat-implementering (Svelte 5, SpacetimeDB, adapter-mønster) - Oppdater ARCHITECTURE.md: Lag 1 status, ny §10 Erfaringslogg, SpacetimeDB i lokal dev - Oppdater synkronisering.md med implementeringsstatus og designvalg - Oppdater lokal.md med SpacetimeDB og AI Gateway - Utvid PG-skjema med channels, messages, media_files, message_revisions - Legg til seed_dev.sql, migration_safety.md, .env.example - Nye feature-specs: chat, kanban, whiteboard, live_ai, lydmeldinger m.fl. - Nye konsept-specs: studioet, møterommet, redaksjonen, den asynkrone gjesten m.fl. - SpacetimeDB og AI Gateway i docker-compose.dev.yml - collect-docs.sh inkluderer erfaringer/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
146 lines
8.6 KiB
Markdown
146 lines
8.6 KiB
Markdown
# Feature Spec: Kunnskapsgraf og Relasjoner (Logseq-modell)
|
|
**Filsti:** `docs/features/kunnskapsgraf_og_relasjoner.md`
|
|
|
|
## 1. Konsept
|
|
Inspirert av verktøy som Logseq og Obsidian, bygger vi databasen som en toveis-lenket graf. Målet er å skape "serendipity" (lykketreff) i research-fasen ved å synliggjøre uventede forbindelser. Hvis Aktør A og Aktør B begge er nevnt i samme chat-tråd eller knyttet til samme Tema over tid, skal systemet kunne visualisere denne røde tråden for programlederne.
|
|
|
|
## 2. Arkitektur og Teknologivalg
|
|
Vi unngår tunge, dedikerte grafdatabaser (som Neo4j) for å holde infrastrukturen og ressursbruken (RAM) minimal.
|
|
* **Valgt teknologi:** Vanilla PostgreSQL.
|
|
* **Mekanisme:** En "Nodes and Edges" (Noder og Kanter) tabellstruktur kombinert med Recursive CTEs (Common Table Expressions) i SQL for å traversere grafen. Dette er mer enn raskt nok for redaksjonelle datamengder (100k+ noder).
|
|
|
|
## 3. Datastruktur
|
|
|
|
### 3.1 Supertabell: `nodes`
|
|
Alle entiteter i systemet arver sin UUID fra én sentral tabell. Dette gir ekte Foreign Key-integritet på `graph_edges` uten applikasjonslogikk-hacks.
|
|
|
|
```sql
|
|
CREATE TYPE node_type AS ENUM (
|
|
'tema', 'aktør', 'faktoide', 'episode', 'segment', 'melding'
|
|
);
|
|
|
|
CREATE TABLE nodes (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
node_type node_type NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
```
|
|
|
|
### 3.2 Detailtabeller
|
|
Hver nodetype har sin egen tabell med FK til `nodes`. Eksempler:
|
|
|
|
```sql
|
|
CREATE TABLE actors (
|
|
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
type TEXT -- 'person', 'organisasjon', etc.
|
|
);
|
|
|
|
CREATE TABLE topics (
|
|
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE episodes (
|
|
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
|
|
title TEXT NOT NULL,
|
|
published_at TIMESTAMPTZ,
|
|
guid TEXT UNIQUE NOT NULL -- RSS <guid>, aldri endres
|
|
);
|
|
|
|
CREATE TABLE segments (
|
|
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
|
|
episode_id UUID NOT NULL REFERENCES episodes(id) ON DELETE CASCADE,
|
|
start_time INTERVAL NOT NULL,
|
|
end_time INTERVAL NOT NULL,
|
|
transcript TEXT, -- Segmentets transkripsjon
|
|
CONSTRAINT valid_timerange CHECK (end_time > start_time)
|
|
);
|
|
|
|
CREATE INDEX idx_segments_episode ON segments(episode_id);
|
|
CREATE INDEX idx_segments_transcript_fts ON segments USING GIN (to_tsvector('norwegian', transcript));
|
|
```
|
|
|
|
### 3.3 Kantene: `graph_edges`
|
|
All kobling skjer i én sentral tabell med ekte FK-integritet. `workspace_id` er denormalisert fra `nodes` for å muliggjøre RLS direkte på edges:
|
|
|
|
```sql
|
|
CREATE TABLE graph_edges (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
source_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
target_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
relation_type TEXT NOT NULL, -- 'MENTIONS', 'CONTRADICTS', 'WORKS_FOR', 'PART_OF', 'DISCUSSED_IN'
|
|
context_id UUID REFERENCES nodes(id) ON DELETE SET NULL,
|
|
confidence REAL CHECK (confidence BETWEEN 0.0 AND 1.0),
|
|
created_by TEXT REFERENCES users(authentik_id) ON DELETE SET NULL,
|
|
origin TEXT NOT NULL DEFAULT 'system', -- 'system', 'user', 'ai'
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT no_self_reference CHECK (source_id != target_id),
|
|
CONSTRAINT unique_edge UNIQUE (source_id, target_id, relation_type)
|
|
);
|
|
|
|
CREATE INDEX idx_edges_source ON graph_edges(source_id);
|
|
CREATE INDEX idx_edges_target ON graph_edges(target_id);
|
|
CREATE INDEX idx_edges_relation ON graph_edges(relation_type);
|
|
CREATE INDEX idx_edges_workspace ON graph_edges(workspace_id);
|
|
```
|
|
|
|
**Merk:** `UNIQUE(source_id, target_id, relation_type)` forhindrer duplikate relasjoner. Bruk `ON CONFLICT DO NOTHING` eller `ON CONFLICT DO UPDATE SET confidence = ...` ved upsert.
|
|
|
|
**Merk:** `node_type`-enumet kan utvides av feature-spesifikke migrasjoner (f.eks. `valgomat_question`, `valgomat_axis`). Se `docs/concepts/valgomaten.md` for eksempel.
|
|
|
|
## 4. Segmenter og Transkripsjoner
|
|
|
|
### 4.1 Segment som grafnode
|
|
Episoder deles i **segmenter** — tidsavgrensede deler med egen transkripsjon. Hvert segment er en node i grafen og kan kobles til Temaer, Aktører og Faktoider. Dette muliggjør presise oppslag som: "I Episode 42, fra 14:23 til 21:07, diskuterte dere Skolepolitikk og sa følgende..."
|
|
|
|
```
|
|
Episode 42 (node: episode)
|
|
├── Segment 00:00-14:22 (node: segment) ──DISCUSSED_IN──► Tema: Mediepolitikk
|
|
├── Segment 14:23-21:07 (node: segment) ──DISCUSSED_IN──► Tema: Skolepolitikk
|
|
│ ──MENTIONS──────► Aktør: Støre
|
|
└── Segment 21:08-45:00 (node: segment) ──DISCUSSED_IN──► Tema: Kommuneøkonomi
|
|
```
|
|
|
|
Kapitler i RSS-feeden genereres fra segmentene, men er et eget konsern (se `docs/concepts/podcastfabrikken.md`).
|
|
|
|
### 4.2 Transkripsjoner: Git som master, PG som søkeindeks
|
|
Transkripsjoner lever i **to steder** med klart eierskap:
|
|
|
|
| Sted | Rolle | Format |
|
|
|---|---|---|
|
|
| **Git (Forgejo)** | Kilde til sannhet. Redigerbar, sporbar, diffbar | Markdown med tidsstempler |
|
|
| **PostgreSQL** | Søkeindeks. Full-text search, koblet til grafen | Segmentert i `segments`-tabellen |
|
|
|
|
**Flyt:**
|
|
```
|
|
Whisper → Git (rå transkripsjon med tidsstempler)
|
|
→ Redaksjonen korrigerer manuelt ved behov
|
|
→ Push til Forgejo
|
|
→ Forgejo webhook trigger 'transcript_reimport'-jobb i jobbkøen
|
|
→ Rust-worker parser filen, splitter i segmenter
|
|
→ DELETE + INSERT i én PG-transaksjon (idempotent reimport)
|
|
→ Grafkoblinger bevares (segment-UUID deterministisk fra episode-UUID + tidsstempel)
|
|
```
|
|
|
|
**Deterministisk UUID for segmenter:** `UUID = uuid_v5(episode_uuid, start_time_ms)`. Dette sikrer at samme segment alltid får samme UUID, selv ved reimport. Grafkoblinger som peker på segmentet overlever dermed en full reimport.
|
|
|
|
## 5. Arbeidsflyt: Hvordan grafen vokser
|
|
Grafen bygger seg opp organisk gjennom daglig bruk av Sidelinja-suiten:
|
|
1. **Chat & Notater:** En bruker skriver: *"Apropos #Hans_Petter_Sjøli, hva var greia med #Arbeiderpartiet?"*
|
|
2. **Parsing (Svelte/Rust):** Systemet fanger opp de to `#`-taggene (som allerede har UUIDs i `Aktør`-tabellen).
|
|
3. **Edge Creation:** SvelteKit server-side oppretter automatisk to nye oppføringer i `graph_edges`-tabellen:
|
|
* [Melding UUID] -> `MENTIONS` -> [Sjøli UUID]
|
|
* [Melding UUID] -> `MENTIONS` -> [Arbeiderpartiet UUID]
|
|
4. **Indirekte relasjon:** Fordi begge aktørene nå deler samme `context_id` (meldingen), vet Kunnskapsgrafen at det finnes en tematisk kobling mellom Sjøli og Ap.
|
|
5. **Publisering:** Når en episode publiseres, kobles segmentene automatisk til relevante Temaer og Aktører basert på AI-analyse av transkripsjonen.
|
|
|
|
## 6. Instruks for Claude Code
|
|
* **Workspace-isolasjon:** Alle noder tilhører en workspace. Opprett alltid noder med riktig `workspace_id`. Spørringer mot detailtabeller (actors, topics, etc.) filtrerer alltid via JOIN med nodes.
|
|
* **`nodes`-tabellen er obligatorisk.** Opprett alltid en rad i `nodes` før du inserter i en detailtabell. Bruk en hjelpefunksjon som gjør begge i én transaksjon.
|
|
* **`graph_edges` krever `workspace_id`.** Ved opprettelse av edges, sett `workspace_id` fra kilde-noden. `UNIQUE(source_id, target_id, relation_type)` hindrer duplikater — bruk `ON CONFLICT` ved upsert.
|
|
* **Graf-spørringer:** Bruk `WITH RECURSIVE` i PostgreSQL når du bygger endepunkter som skal hente ut "Linked Mentions" eller nettverket rundt en spesifikk Aktør opp til 2-3 ledd ut.
|
|
* **Fremtidssikring for UI:** Design JSON-responsen slik at den lett kan mates inn i graf-visualiseringsbiblioteker (som D3.js eller Vis.js) i Svelte-frontenden. Formatet bør være `{ "nodes": [...], "edges": [...] }`.
|
|
* **Transkripsjon-reimport:** Workeren må være idempotent. Bruk `uuid_v5(episode_uuid, start_time_ms)` for deterministiske segment-UUIDs. Slett og gjenopprett segmenter i én transaksjon, men **ikke** slett edges som peker til segmentene — de overlever fordi UUID-en er stabil. **Merk:** Hvis manuelle korrigeringer endrer segment-grenser (start_time), endres UUID-en. Løsning: automatisk flytt eksisterende edges til nærmeste nye segment basert på tidsintervall-overlapp.
|
|
* **Full-text search:** Bruk `to_tsvector('norwegian', transcript)` for norsk språkstøtte i søk.
|