server/docs/features/kunnskapsgraf_og_relasjoner.md
vegard a5985ef3f8 Dokumentasjon, erfaringslogg, migrasjoner og infra-oppdateringer
- 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>
2026-03-15 01:40:14 +01:00

8.6 KiB

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.

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:

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:

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.