484 lines
17 KiB
Markdown
484 lines
17 KiB
Markdown
# Munin
|
|
|
|
Ein projektbezogenes Gedächtnis für KI-Agenten und Menschen.
|
|
|
|
> In der nordischen Überlieferung sind Hugin und Munin die Raben Odins. Munin
|
|
> steht für das Gedächtnis: Er fliegt aus, sammelt und kehrt zurück, um zu
|
|
> berichten — was hier die Agenten tun. Passend zum Torwächter **Heimdall**,
|
|
> unserem AI-Gateway.
|
|
|
|
KI-Werkzeuge (Claude, Cline, OpenCode, eigene Anwendungen) legen darin über
|
|
**MCP** ab, was sie lernen, und rufen es wieder auf. Menschen pflegen dasselbe
|
|
Wissen über eine schlanke Weboberfläche. Beides landet im selben Speicher — der
|
|
Unterschied ist ein Feld am Eintrag, kein zweites System.
|
|
|
|
```
|
|
KI-Werkzeug ──MCP──┐
|
|
├──▶ Projekt ──▶ Einträge (SQLite)
|
|
Browser ──HTTP─────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Inhalt
|
|
|
|
- [Schnellstart](#schnellstart)
|
|
- [Grundbegriffe](#grundbegriffe)
|
|
- [MCP einrichten](#mcp-einrichten)
|
|
- [Die Werkzeuge](#die-werkzeuge)
|
|
- [Weboberfläche](#weboberfläche)
|
|
- [Konfiguration](#konfiguration)
|
|
- [Sicherheit](#sicherheit)
|
|
- [Betrieb](#betrieb)
|
|
- [Entwicklung](#entwicklung)
|
|
|
|
---
|
|
|
|
## Schnellstart
|
|
|
|
```bash
|
|
# 1. Konfiguration anlegen
|
|
cp .env.example .env
|
|
|
|
# 2. Geheimnisse erzeugen und eintragen
|
|
openssl rand -hex 32 # → SESSION_SECRET
|
|
openssl rand -hex 24 # → ADMIN_TOKEN
|
|
|
|
# 3. Starten
|
|
go run .
|
|
```
|
|
|
|
Dann `http://localhost:8080` öffnen und mit dem `ADMIN_TOKEN` anmelden.
|
|
|
|
Der weitere Weg:
|
|
|
|
1. **Projekt anlegen** unter *Projekte*
|
|
2. **Zugang erzeugen** — der Key wird genau einmal angezeigt
|
|
3. **MCP-Client einrichten** (die Anleitung erscheint direkt neben dem neuen Key)
|
|
|
|
---
|
|
|
|
## Grundbegriffe
|
|
|
|
| Begriff | Bedeutung |
|
|
|---|---|
|
|
| **Projekt** | Die Einheit der Abschottung. Eigene Einträge, eigene Zugänge, eigener MCP-Endpunkt. Projekte sehen sich gegenseitig nicht. |
|
|
| **Eintrag** | Ein Stück Wissen: Bezeichner, Titel, Markdown-Inhalt, Schlagworte. Von Menschen oder Agenten geschrieben. |
|
|
| **Bezeichner** (slug) | Kurzer, dauerhafter Name eines Eintrags (`deployment-prozess`). Der Schlüssel gegen Dubletten: Zweimal derselbe Bezeichner aktualisiert, statt eine zweite Fassung danebenzustellen. |
|
|
| **API-Key** | Zugang zu genau einem Projekt, mit Namen und Rolle. Wird nur als Prüfsumme gespeichert. |
|
|
| **Plattform-Admin** | Der `ADMIN_TOKEN`. Legt Projekte an, kommt an jedes Projekt. |
|
|
|
|
### Warum Bezeichner statt Ordner
|
|
|
|
Ein Wiki gliedert nach Ordnern. Für ein Gedächtnis, in das Agenten schreiben,
|
|
funktioniert das nicht: Wo eine Erkenntnis „hingehört", ist selten eindeutig, und
|
|
ein Agent trifft diese Entscheidung schlecht.
|
|
|
|
Stattdessen gibt es **Schlagworte** zum Einordnen und **Verweise** zum
|
|
Verknüpfen. Im Inhalt zeigt `[[bezeichner]]` auf einen anderen Eintrag; die
|
|
Gegenrichtung („was verweist hierher") entsteht daraus von selbst und steht in
|
|
der Detailansicht wie auch in der MCP-Antwort.
|
|
|
|
---
|
|
|
|
## MCP einrichten
|
|
|
|
Nach dem Erzeugen eines Keys zeigt die Weboberfläche den passenden Abschnitt an.
|
|
Der Key bestimmt das Projekt — ein zusätzlicher Parameter ist nicht nötig. Wer mit
|
|
mehreren Projekten arbeitet, trägt mehrere Server mit je eigenem Key ein.
|
|
|
|
Alle Clients sprechen denselben Endpunkt `…/mcp` über Streamable HTTP an. Die
|
|
Konfigurationsdateien sehen trotzdem unterschiedlich aus:
|
|
|
|
| Client | Datei | Schlüssel | Transport heißt dort |
|
|
|---|---|---|---|
|
|
| Claude Code | `.mcp.json` / `~/.claude.json` | `mcpServers` | `"type": "http"` |
|
|
| Cline | `cline_mcp_settings.json` / `~/.cline/mcp.json` | `mcpServers` | `"type": "streamableHttp"` |
|
|
| OpenCode | `opencode.json` | `mcp` | `"type": "remote"` |
|
|
|
|
Die beiden ersten Zeilen sind die Falle: gleicher Schlüssel, anderer Wert für
|
|
`type`. Ein aus Claude Code kopierter Block läuft in Cline nicht.
|
|
|
|
### Claude Code
|
|
|
|
Am schnellsten über die Kommandozeile:
|
|
|
|
```bash
|
|
claude mcp add --transport http munin-plattform https://munin.intern/mcp \
|
|
--header "Authorization: Bearer wk_…"
|
|
```
|
|
|
|
Ohne `--scope` landet der Eintrag in `~/.claude.json` und gilt nur im aktuellen
|
|
Projekt; `--scope user` macht ihn überall verfügbar.
|
|
|
|
Fürs Team gehört er stattdessen als `.mcp.json` ins Repository — dann aber
|
|
**ohne** den Key im Klartext. Claude Code löst `${VAR}` aus der Umgebung auf:
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"munin-plattform": {
|
|
"type": "http",
|
|
"url": "https://munin.intern/mcp",
|
|
"headers": { "Authorization": "Bearer ${MUNIN_KEY}" }
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Jede Person setzt `MUNIN_KEY` auf ihren eigenen Key. Damit steht im Repository die
|
|
Einrichtung, nicht das Geheimnis — und im Protokoll steht trotzdem, wer geschrieben
|
|
hat.
|
|
|
|
### Cline
|
|
|
|
Über die Oberfläche: *MCP Servers* → *Configure* → *Configure MCP Servers*. In der
|
|
Kommandozeilenfassung liegt dieselbe Datei unter `~/.cline/mcp.json`.
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"munin-plattform": {
|
|
"type": "streamableHttp",
|
|
"url": "https://munin.intern/mcp",
|
|
"headers": { "Authorization": "Bearer wk_…" },
|
|
"disabled": false,
|
|
"autoApprove": ["recall", "get", "list"]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
`autoApprove` nennt die Werkzeuge, die ohne Rückfrage laufen dürfen. Die drei
|
|
lesenden gehören dort hinein — sonst muss jede Suche einzeln bestätigt werden, und
|
|
ein Agent, der für jeden Blick ins Gedächtnis nachfragen muss, benutzt es nicht.
|
|
`remember` und `forget` gehören ausdrücklich **nicht** hinein: Sie verändern
|
|
gemeinsames Wissen.
|
|
|
|
### OpenCode
|
|
|
|
`opencode.json` im Projektverzeichnis oder global unter
|
|
`~/.config/opencode/opencode.json`. Anderes Schema als bei den beiden anderen —
|
|
der Block heißt `mcp`, nicht `mcpServers`:
|
|
|
|
```json
|
|
{
|
|
"$schema": "https://opencode.ai/config.json",
|
|
"mcp": {
|
|
"munin-plattform": {
|
|
"type": "remote",
|
|
"url": "https://munin.intern/mcp",
|
|
"enabled": true,
|
|
"headers": { "Authorization": "Bearer {env:MUNIN_KEY}" }
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
OpenCode ersetzt `{env:VAR}` aus der Umgebung und `{file:~/pfad}` durch den Inhalt
|
|
einer Datei. Ein nicht gesetztes `{env:…}` wird zur leeren Zeichenkette — der
|
|
Server antwortet dann mit `401`, nicht mit einer Fehlermeldung über die
|
|
Konfiguration.
|
|
|
|
### Danach
|
|
|
|
**Ein Key je Person und je Agent.** Nur so steht im Protokoll, wer etwas
|
|
geschrieben hat, und ein einzelner Zugang lässt sich zurückziehen, ohne alle
|
|
anderen zu stören.
|
|
|
|
Verbindung prüfen: Der Agent sollte `recall`, `get`, `list`, `remember` und
|
|
`forget` anbieten. Tut er das nicht, hilft ein Blick ins Zugriffsprotokoll des
|
|
Servers — ein `401` dort heißt falscher oder zurückgezogener Key, gar kein Eintrag
|
|
heißt, dass der Client den Server nie aufgerufen hat.
|
|
|
|
Wie man Agenten so anspricht, dass sie den Speicher auch nutzen:
|
|
[AGENTEN-GUIDE.md](AGENTEN-GUIDE.md).
|
|
|
|
---
|
|
|
|
## Die Werkzeuge
|
|
|
|
| Werkzeug | Rolle | Zweck |
|
|
|---|---|---|
|
|
| `recall` | read | Durchsucht den Speicher. Der Haupteinstieg — auch vor dem Schreiben. |
|
|
| `get` | read | Lädt einen Eintrag vollständig, samt Verweisen und Fassungsnummer. |
|
|
| `list` | read | Überblick: zuletzt geändert, alle Schlagworte. |
|
|
| `remember` | write | Legt einen Eintrag an oder aktualisiert ihn. |
|
|
| `forget` | write | Löscht einen Eintrag. |
|
|
|
|
Der Server schickt jedem Client eine **Anleitung** (`instructions`) mit, die das
|
|
Vorgehen beschreibt — vor allem: *vor* dem Schreiben erst `recall` aufrufen. Das
|
|
ist das wirksamste Mittel gegen Dubletten und steht in
|
|
`internal/mcpserver/server.go`.
|
|
|
|
### Schreibbudget
|
|
|
|
`remember` und `forget` sind je Projekt auf **1.000 Aufrufe pro Stunde** gedeckelt
|
|
(`WRITE_LIMIT_HOURLY`). Lesen ist nie begrenzt.
|
|
|
|
Der Grund ist die Kehrseite einer anderen Entscheidung: Ein gültiger Key umgeht
|
|
die IP-Sperre bewusst, weil Deploy-Skript, MCP-Client und Browser eines Büros
|
|
sich nach außen oft eine Adresse teilen. Damit hätte ein Agent, der in einer
|
|
Schleife hängt, sonst überhaupt keine Grenze.
|
|
|
|
Das Budget hängt am **Projekt**, nicht am Key: Ein Limit je Key ließe sich
|
|
umgehen, indem man einen zweiten Key anlegt, und das darf jeder Projekt-Admin
|
|
selbst. Einzelne Projekte bekommen über *Projekte → Bearbeiten* eine eigene
|
|
Obergrenze; die Übersicht zeigt, wie viel im laufenden Fenster noch frei ist.
|
|
Ändern darf sie nur der Plattform-Admin — aus demselben Grund.
|
|
|
|
Ist das Budget erschöpft, bekommt das Modell keinen nackten Fehler, sondern die
|
|
Anweisung, abzubrechen und das Limit zu melden — sonst versucht es sofort wieder
|
|
oder weicht auf ein anderes Werkzeug aus.
|
|
|
|
### Gleichzeitiges Schreiben
|
|
|
|
`get` liefert eine `version`. Wird sie bei `remember` als `expected_version`
|
|
mitgegeben, meldet der Server einen Konflikt, falls jemand anderes den Eintrag
|
|
inzwischen geändert hat — statt die fremde Änderung stillschweigend zu
|
|
überschreiben.
|
|
|
|
```
|
|
Agent A: get(deployment) → version 3
|
|
Agent B: get(deployment) → version 3
|
|
Agent A: remember(…, expected_version=3) → OK, jetzt version 4
|
|
Agent B: remember(…, expected_version=3) → Konflikt: neu laden, zusammenführen
|
|
```
|
|
|
|
---
|
|
|
|
## Weboberfläche
|
|
|
|
| Weg | Zweck |
|
|
|---|---|
|
|
| `/login` | Anmeldung mit API-Key oder Admin-Token |
|
|
| `/admin/projects` | Projekte anlegen und auflisten (nur Plattform-Admin) |
|
|
| `/admin/projects/{projekt}/edit` | Name, Beschreibung und Schreibbudget ändern; Projekt löschen |
|
|
| `/p/{projekt}/` | Einträge durchsuchen, nach Schlagwort filtern |
|
|
| `/p/{projekt}/e/{bezeichner}` | Eintrag lesen, mit Rückverweisen |
|
|
| `/p/{projekt}/keys` | Zugänge verwalten (Rolle `admin`) |
|
|
| `/p/{projekt}/audit` | Protokoll des Projekts |
|
|
| `/healthz` | Healthcheck, ohne Anmeldung |
|
|
|
|
### Rollen
|
|
|
|
| Rolle | Darf |
|
|
|---|---|
|
|
| `read` | lesen, suchen |
|
|
| `write` | zusätzlich schreiben und löschen |
|
|
| `admin` | zusätzlich Zugänge des eigenen Projekts verwalten |
|
|
|
|
---
|
|
|
|
## Konfiguration
|
|
|
|
| Variable | Standard | Bedeutung |
|
|
|---|---|---|
|
|
| `SESSION_SECRET` | — | **Pflicht.** Signiert Sitzungen und CSRF-Tokens. Mindestens 32 Zeichen. |
|
|
| `ADMIN_TOKEN` | — | **Pflicht.** Zugang des Plattform-Admins. Mindestens 16 Zeichen. |
|
|
| `DB_PATH` | `data/munin.db` | Pfad zur SQLite-Datenbank |
|
|
| `PORT` | `8080` | Port des Servers |
|
|
| `COOKIE_SECURE` | automatisch | `Secure`-Flag der Cookies. Ohne Angabe: an, außer bei localhost. |
|
|
| `ENABLE_HSTS` | `false` | HSTS-Header senden. Nur bei gültigem Zertifikat. |
|
|
| `TRUSTED_PROXY` | — | IPs/CIDR-Netze, deren `X-Forwarded-For` geglaubt wird |
|
|
| `MAX_API_BODY_BYTES` | `4194304` | Höchstgröße eines MCP-Requests (4 MB) |
|
|
| `WRITE_LIMIT_HOURLY` | `1000` | Schreibende MCP-Aufrufe je Projekt und Stunde. `0` = unbegrenzt. |
|
|
| `EXPORT_DIR` | — | Zielverzeichnis des Markdown-Exports. Leer = abgeschaltet. |
|
|
|
|
Fehlen die Pflichtangaben oder sind sie zu kurz, startet der Server nicht. Ein
|
|
schwaches Geheimnis wäre die Wurzel für alles andere — ein klarer Abbruch ist
|
|
besser als unsicherer Betrieb.
|
|
|
|
---
|
|
|
|
## Sicherheit
|
|
|
|
### Was geprüft wird
|
|
|
|
| Schutz | Wo |
|
|
|---|---|
|
|
| Signierte Sitzungs-Cookies (HMAC-SHA256) | `internal/auth/session.go` |
|
|
| API-Keys nur als SHA-256-Prüfsumme gespeichert | `internal/auth/keys.go` |
|
|
| Zurückgezogener Key beendet laufende Sitzungen sofort | `internal/web/middleware.go` |
|
|
| CSRF-Token in allen Formularen | `internal/auth/csrf.go` |
|
|
| `SameSite=Lax`, `HttpOnly`, `Secure` | `internal/auth/cookies.go` |
|
|
| Rate-Limiting je Adresse, nur auf Fehlversuche | `internal/auth/ratelimit.go` |
|
|
| Content-Security-Policy und weitere Schutz-Header | `internal/web/middleware.go` |
|
|
| Größenlimits für Request-Bodies | `internal/web/middleware.go` |
|
|
| Server-Timeouts gegen Slowloris | `main.go` |
|
|
| Optimistisches Sperren gegen stilles Überschreiben | `internal/store/entries.go` |
|
|
|
|
### Abschottung der Projekte
|
|
|
|
Sie ist **strukturell**, nicht eine Frage der richtigen Filterbedingung:
|
|
|
|
- Jedes Projekt hat einen **eigenen MCP-Server**, dessen Werkzeuge fest auf
|
|
dieses Projekt verdrahtet sind
|
|
- Jedes Projekt hat einen **eigenen Suchindex** — auch die BM25-Gewichtung wird
|
|
nicht über Projektgrenzen hinweg berechnet
|
|
- Jeder Datenbankzugriff ist auf die Projekt-ID eingeschränkt
|
|
- Ein fremdes Projekt antwortet mit **404, nicht 403** — sonst ließe sich über
|
|
die Statuscodes herausfinden, welche Projekte es sonst noch gibt
|
|
|
|
### Warum SHA-256 für Keys und nicht bcrypt
|
|
|
|
Für Passwörter wären langsame Verfahren richtig: Menschen wählen aus einem
|
|
kleinen, erratbaren Raum. Ein API-Key hier ist aber 256 Bit aus `crypto/rand` —
|
|
diesen Raum durchsucht niemand. Die Langsamkeit brächte keinen Gewinn, würde aber
|
|
jeden Request bremsen. Entscheidend ist der zweite Punkt: Weil SHA-256 schnell
|
|
und deterministisch ist, wird der Key in **einem indizierten Lookup** aufgelöst
|
|
statt in einer Schleife über alle Keys.
|
|
|
|
### Prompt Injection
|
|
|
|
Der Inhalt des Speichers landet direkt im Kontext von KI-Werkzeugen. Wer in ein
|
|
Projekt schreiben darf, schreibt damit indirekt in die Prompts aller anderen, die
|
|
dieses Projekt nutzen.
|
|
|
|
Dagegen hilft kein technischer Schutz, nur Nachvollziehbarkeit: Jeder Eintrag
|
|
trägt Herkunft (`human`/`agent`) und den Namen des verwendeten Zugangs, und jede
|
|
Änderung steht im Protokoll.
|
|
|
|
### Wo die Grenzen liegen
|
|
|
|
- **Keine Benutzerkonten.** Zugänge sind Keys, keine Personen. Ein Key je Person
|
|
ist der Ersatz — geplant ist später Keycloak.
|
|
- **Projekte pflegt nur der Plattform-Admin.** Ein Team kann seinen eigenen
|
|
Projektnamen nicht selbst korrigieren. Solange es wenige Projekte gibt, ist
|
|
eine Berechtigungsstufe weniger den Umweg wert.
|
|
- **Kein TLS im Server selbst.** Für HTTPS gehört ein Reverse-Proxy davor.
|
|
- **Ein Workspace.** Die Struktur für mehrere ist im Schema angelegt
|
|
(`workspace_id`), aber nicht in der Oberfläche.
|
|
- **Das Schreibbudget zählt im Arbeitsspeicher.** Nach einem Neustart steht es
|
|
wieder bei null. Als Netz gegen eine Schleife reicht das; eine verbindliche
|
|
Zusage ist es nicht.
|
|
|
|
---
|
|
|
|
## Betrieb
|
|
|
|
### Docker
|
|
|
|
```bash
|
|
docker build -t munin .
|
|
|
|
docker run -d --name munin \
|
|
-p 8080:8080 \
|
|
-v munin-daten:/data \
|
|
-e SESSION_SECRET="$(openssl rand -hex 32)" \
|
|
-e ADMIN_TOKEN="$(openssl rand -hex 24)" \
|
|
-e ENABLE_HSTS=true \
|
|
-e TRUSTED_PROXY=172.16.0.0/12 \
|
|
munin
|
|
```
|
|
|
|
Die Geheimnisse sollten aus einem Passwortspeicher kommen, nicht bei jedem Start
|
|
neu erzeugt werden — ein neues `SESSION_SECRET` macht alle Sitzungen ungültig.
|
|
|
|
### Hinter einem Reverse-Proxy
|
|
|
|
```bash
|
|
TRUSTED_PROXY=127.0.0.1 # Adresse des Proxys
|
|
COOKIE_SECURE=true # falls der Proxy TLS beendet
|
|
ENABLE_HSTS=true # nur mit gültigem Zertifikat
|
|
```
|
|
|
|
Ohne `TRUSTED_PROXY` sieht der Server für alle Besucher dieselbe Adresse — das
|
|
Rate-Limiting würde dann entweder niemanden bremsen oder beim ersten Angreifer
|
|
gleich alle aussperren.
|
|
|
|
### Datensicherung
|
|
|
|
**Datenbank.** Alles steckt in einer Datei. Weil SQLite im WAL-Modus läuft, sollte
|
|
sie nicht im laufenden Betrieb kopiert werden:
|
|
|
|
```bash
|
|
sqlite3 /pfad/munin.db ".backup '/sicherung/wissen-$(date +%F).db'"
|
|
```
|
|
|
|
**Markdown-Export.** Ist `EXPORT_DIR` gesetzt, schreibt der Server den Bestand
|
|
täglich als Markdown-Dateien dorthin — ein Ordner je Projekt, eine Datei je
|
|
Eintrag, dazu eine `_index.md` als Übersicht:
|
|
|
|
```
|
|
export/
|
|
plattform/
|
|
_index.md
|
|
deployment.md
|
|
server-zugang.md
|
|
```
|
|
|
|
Jede Datei trägt alle Felder im Kopf (Bezeichner, Schlagworte, Herkunft, Autor,
|
|
Fassung, Zeitstempel), sodass sich der Bestand daraus auch wieder einlesen ließe.
|
|
|
|
Das eignet sich als Git-Sicherung: Ein Lauf ohne inhaltliche Änderungen fasst
|
|
keine Datei an und erzeugt deshalb auch keinen Diff.
|
|
|
|
```bash
|
|
# Auf Knopfdruck: "Jetzt exportieren" unter /admin/projects
|
|
# Für Cron oder ein Sicherungsskript:
|
|
./munin -export
|
|
```
|
|
|
|
Der Export ist eine Sicherung, keine zweite Wahrheit — Änderungen an den Dateien
|
|
wirken nicht zurück und gehen beim nächsten Lauf verloren.
|
|
|
|
### Protokoll
|
|
|
|
Vorgänge werden 90 Tage aufbewahrt und danach automatisch gelöscht
|
|
(`auditRetention` in `main.go`). Das Protokoll enthält Adressen und Zugangsnamen,
|
|
aus denen sich Personen ableiten lassen — eine Aufbewahrungsfrist ist deshalb die
|
|
Voraussetzung dafür, es überhaupt führen zu dürfen.
|
|
|
|
---
|
|
|
|
## Entwicklung
|
|
|
|
```bash
|
|
go run . # starten
|
|
go test ./... # alle Tests
|
|
go vet ./... # statische Prüfung
|
|
gofmt -l . # Formatierung prüfen
|
|
```
|
|
|
|
### Aufbau
|
|
|
|
```
|
|
main.go Konfiguration, Verdrahtung, Serverstart
|
|
internal/
|
|
store/ SQLite: Schema, Migrationen, alle Abfragen
|
|
entry/ Fachlogik: Markdown, Verweise, Textausschnitte
|
|
search/ BM25-Volltextindex, einer je Projekt
|
|
auth/ Sitzungen, CSRF, Cookies, Keys, Rate-Limiting
|
|
export/ Markdown-Export als Sicherung
|
|
web/ Routen, Middleware, Handler
|
|
mcpserver/ MCP-Werkzeuge
|
|
templates/ static/ eingebettet ins Binary
|
|
```
|
|
|
|
Die Richtung der Abhängigkeiten zeigt immer nach innen: `web` und `mcpserver`
|
|
kennen `store`, aber `store` kennt keinen von beiden.
|
|
|
|
### Datenbank
|
|
|
|
SQLite über `modernc.org/sqlite` — ein in Go übersetzter Nachbau, kein C. Nur
|
|
deshalb lässt sich das Programm als statisches Binary ohne Laufzeitumgebung
|
|
ausliefern.
|
|
|
|
Das SQL vermeidet bewusst alles, was nur SQLite kann (kein `AUTOINCREMENT`, keine
|
|
Datumsfunktionen, keine `INSERT OR REPLACE`), damit ein Umstieg auf PostgreSQL
|
|
offenbleibt. Die Regeln stehen als Kommentar in
|
|
`internal/store/migrations/001_init.sql`.
|
|
|
|
**Schema ändern:** eine neue Datei `003_….sql` in `migrations/` anlegen. Sie wird
|
|
beim nächsten Start automatisch angewandt und vermerkt; bestehende Migrationen
|
|
werden nie verändert.
|
|
|
|
---
|
|
|
|
## Was noch aussteht
|
|
|
|
[AUSBAUSTUFEN.md](AUSBAUSTUFEN.md) hält fest, was bewusst nicht gebaut wurde —
|
|
mit Messwerten, Begründung und den Auslösern, an denen man merkt, dass es Zeit
|
|
dafür ist. Unter anderem: FTS5 statt eigenem Suchindex, eine Datenbank je
|
|
Projekt, semantische Suche.
|