COMPASS-Doku

Ein Regelwerk importieren

Was der Importer heute wirklich erkennt

POST /import/claude-md ist ein deterministischer Scanner ohne Sprachmodell — kein vollständiger Markdown-Parser, und ohne Wissen über die Struktur einer bestimmten Datei. Er versucht zwei Wege, in dieser Reihenfolge:

  1. Das nummerierte Regelformat zuerst: ein ## 0. ...-Abschnitt mit ### R<n> — Titel (P<n>)-Überschriften darin. Findet dieser Weg mindestens eine Regel, laufen ausschließlich diese Treffer — regel_text ist dann der erste Anweisungssatz des ersten Prosa-Absatzes unter der jeweiligen Überschrift (Metadaten-Absätze wie **SoT:** ... oder **Erzwingungsgrad:** ... werden übersprungen). Ein ### R<n>-Header ohne (P<n>)-Suffix im Titel (z. B. eine aufgehobene/reservierte Nummer wie R12) liefert trotzdem einen Vorschlag, wird aber zusätzlich in unklar gemeldet.
  2. Sonst — der übliche Fall — Freiform-Rückfall: nur wenn Weg 1 nichts liefert (kein ## 0.-Abschnitt oder keine ### R<n>-Überschrift darin), sucht derselbe Aufruf genau zwei freie Formen:
  3. Eine Überschrift (# bis ####), gefolgt von einem Prosa-Absatz: jeder Satz in diesem Absatz, der mit einem Imperativ-Marker beginnt, wird zum Kandidaten.
  4. Ein Stichpunkt oder nummeriertes Listenelement, dessen Text mit einem Imperativ-Marker beginnt.

Die Marker sind fest und groß-/kleinschreibungsunabhängig — Englisch: Always, Never, Do not, Don't, Use, Prefer, Must, Avoid; Deutsch: Immer, Nie, Niemals, Kein, Keine, Nutze, Verwende, Vermeide, Muss. Code-Blöcke (```) und Tabellenzeilen (Zeilen mit führendem |) sind nie eine Quelle.

Jeder Vorschlag trägt den Originalwortlaut, keine Umformulierung: regel_text ist genau der gefundene Satz bzw. Stichpunkttext (oder, beim nummerierten Regelformat, der erste Anweisungssatz), nur von Markdown (**fett**, `code`, [links](url)) befreit. titel ist beim nummerierten Regelformat der Titel aus der ### R<n> — Titel-Überschrift, beim Freiform-Weg derselbe Text wie regel_text, gekürzt auf die ersten 8 Wörter für einen lesbaren Sachen-Titel; sache ist ein daraus abgeleiteter Kebab-Slug, bei Kollision mit -2, -3, … eindeutig gemacht.

Ehrlich benannte Grenzen

  • Die Sonderbehandlung für das nummerierte Regelformat greift nur innerhalb eines ## 0. ...-Abschnitts. ### R<n> — Titel-Überschriften außerhalb eines solchen Abschnitts (oder ganz ohne (P<n>)-Konvention) werden nicht gefunden — dafür bräuchte es entweder den ## 0.-Rahmen oder den Freiform-Rückfall (der dann keine Sonderbehandlung für die Regelnummer kennt: sie landet im Titel, wenn der Prosa-Satz selbst mit einem Imperativ-Marker beginnt). Weder der Überschriftentext noch eine Regelnummer werden für sich allein zu einem Vorschlag — eine Überschrift ohne markerführenden Satz darunter (Freiform-Weg) oder ganz ohne Prosa-Absatz (nummeriertes Regelformat) liefert nichts. Viele Kernsatz-Absätze im Freiform-Weg beginnen zufällig nicht mit einem erkannten Marker-Wort (sie beschreiben die Regel, statt sie zu befehlen), und sie werden nicht zu einem Vorschlag. Seit dem 25.09.2026 werden sie aber auch nicht mehr stillschweigend übergangen: unklar nennt sie gezählt (siehe unten). Bei so einer Datei die Vorschlagsliste als Teiltreffer behandeln — wie viele Stellen daneben liegen, steht jetzt in der Antwort. (Ein separates, eigenständiges Werkzeug trägt dieselbe Zwei-Wege-Logik — es ist aber über die API nicht erreichbar.)
  • Die Marker-Erkennung im Freiform-Weg ist rein lexikalisch, genau am Anfang des Satzes oder Stichpunkts — sie versteht nicht, was der Rest des Satzes sagt. „Use of tabs is discouraged in this repository." beginnt mit Use und wird deshalb zum Vorschlag, obwohl der Satz gerade keine Handlungsanweisung ist. Jeden regel_text lesen, bevor er übernommen wird.
  • Prosa ohne Marker am Anfang wird nie zur Regel, egal wie klar sie eine Vorgabe formuliert. Entweder die Quelle mit einem führenden Marker umformulieren, oder die Aussage danach von Hand schreiben (siehe Regel setzen und ablösen).
  • Ein Stichpunkt, dessen Marker nicht am Anfang steht (z. B. „Siehe die Doku — ihr müsst sie immer zuerst prüfen") wird nicht still verworfen: er landet als Beinahe-Treffer in unklar — diese Liste vor der Annahme prüfen, die Vorschläge seien die ganze Geschichte.

Was unklar sagt — zwei Arten Eintrag

Seit dem 25.09.2026 verschweigt die Liste nichts mehr. Sie enthält, in dieser Reihenfolge:

  1. Einzelne Zweifelsfälle, je einer mit Zeilennummer — eine Stelle, die fast eine Regel war (der Marker stand nur nicht am Anfang).
  2. Je Grund eine gezählte Zeile mit Anzahl und erstem Beispiel samt Zeilennummer, für alles, was gar nicht als Regel gelesen wurde: Zitatblöcke, Tabellen, Stichpunkte ganz ohne Marker, Absätze ohne Überschrift darüber, und Sätze unter einer Überschrift ohne führenden Marker.

Ein Beispiel, gemessen an der CLAUDE.md von COMPASS selbst (189 Zeilen):

7 Vorschläge
4 unklar:
  9 Zeilen wurden nicht als Regel gelesen: in Zitatblöcken (> ...) …
  8 Zeilen wurden nicht als Regel gelesen: in Tabellen …
  8 Stellen wurden nicht als Regel gelesen: Stichpunkte ohne Imperativ-Marker …
  60 Stellen wurden nicht als Regel gelesen: Sätze unter einer Überschrift, die
     nicht mit einem Imperativ-Marker beginnen …

Vorher stand dort 0 unklar bei denselben 7 Vorschlägen. Gezählt und nicht einzeln aufgeführt, weil 85 Zeilen niemand liest — wer eine bestimmte Stelle sucht, hat die Zeilennummer des ersten Falls je Grund.

art: derselbe Scan, ein anderes Schreiben

art (Formularfeld, Standard claude-md) ist claude-md | agents-md | cursor-rules. Alle drei laufen durch genau denselben Scan oben — nur was apply anschließend schreibt, unterscheidet sich (s. u.). agents-md/ cursor-rules verwenden, wenn diese Dateien statt einer CLAUDE.md importiert werden.

Schritt 1 — Vorschau

curl -sS -X POST "$COMPASS_URL/import/claude-md" \
  -H "X-Compass-Key: $COMPASS_KEY_RO" \
  -F 'text=## Security

- Always encrypt data at rest.
- Never log a password in plain text.'

Es wird noch nichts geschrieben. Die Antwort:

{
  "neu": [
    {"sache": "always-encrypt-data-at-rest", "titel": "Always encrypt data at rest",
     "frage": "Was gilt für 'Always encrypt data at rest'?",
     "regel_text": "Always encrypt data at rest.", "quelle_zeile": 3},
    {"sache": "never-log-a-password-in-plain-text", "titel": "Never log a password in plain text",
     "frage": "Was gilt für 'Never log a password in plain text'?",
     "regel_text": "Never log a password in plain text.", "quelle_zeile": 4}
  ],
  "unveraendert": [],
  "weicht_ab": [],
  "unklar": [],
  "art": "claude-md"
}

Vier Gruppen, und sie sind der eigentliche Zweck dieses Schritts. Die Vorschau liest den Bestand und sagt je Vorschlag, wie er sich zu dem verhält, was bei euch schon gilt:

Gruppe Bedeutung Was apply damit tut
neu zu der Sache gilt noch keine Aussage dieser Art anlegen
unveraendert es gilt bereits derselbe Wortlaut nichts — wird nicht zweimal geschrieben
weicht_ab es gilt ein anderer Wortlaut die geltende Aussage wird abgelöst (ersetzt)
unklar der Scanner war sich nicht sicher nichts, nur anzeigen

Einträge in unveraendert und weicht_ab tragen zusätzlich geltende mit id, text, gilt_seit und quelle — ihr seht vor dem Anwenden, was genau abgelöst würde.

Damit ist ein zweiter Import derselben Datei nicht mehr dieselbe Arbeitsliste wie der erste.

unklar lesen, bevor irgendetwas angewendet wird — die Liste nennt, was der Scanner gesehen, aber nicht sicher genug für einen Vorschlag fand.

Wenn die Datei gar keine Regeldatei ist

Der Datei-Weg (-F "file=@…") prüft vor dem Zerlegen zweierlei und liest im Fehlerfall nichts:

Fall Antwort
größer als 1 MB 422 mit feld: "file", byte (die gemessene Größe) und grenze
keine Textdatei 422 mit feld: "file" und byte — der Byte-Versatz, ab dem es kippt (kein Zeichen-Index)

„Keine Textdatei" heißt: entweder ist das Byte an dieser Stelle kein gültiges UTF-8, oder dort steht ein Nullbyte (eine UTF-16-Datei ist streckenweise gültiges UTF-8 und trotzdem keine Textdatei).

Es wird nichts geraten: kein zweiter Versuch mit einer anderen Kodierung, kein Kürzen auf die Grenze. Eine falsch geratene Kodierung schriebe „spätestens" als „sp?testens" in euer Regelwerk, und eine stumm verstümmelte Regel ist teurer als eine abgelehnte Datei.

Eine Grenze, damit sie niemanden überrascht: sache entsteht aus dem Titel. Bei Stichpunkten ist der Stichpunkt der Titel — wer eine Stichpunkt-Regel umformuliert, erzeugt damit einen neuen Slug, und der Vorschlag erscheint als neu, nicht als weicht_ab; die alte Regel gilt weiter. Nur wo der Titel getrennt vom Regeltext steht (Überschriften, ### R<n> — Titel), erkennt der Abgleich eine Umformulierung.

Schritt 2 — prüfen, dann anwenden

POST /import/claude-md/apply schreibt sowohl die Sache als auch ihre Aussage — eine regel bei art=claude-md, ein fakt bei art=agents-md/art=cursor-rules (s. u.). Die Vorschläge direkt als items durchreichen:

curl -sS -X POST "$COMPASS_URL/import/claude-md/apply" \
  -H "X-Compass-Key: $COMPASS_KEY_RW" -H "Content-Type: application/json" \
  -d '{"items": [
    {"sache": "always-encrypt-data-at-rest", "titel": "Always encrypt data at rest",
     "frage": "Was gilt für '"'"'Always encrypt data at rest'"'"'?",
     "regel_text": "Always encrypt data at rest.", "quelle_zeile": 3}
  ]}'

Oder stattdessen per Slug auswählen, mit demselben Original-text — die Vorschau wird dann serverseitig erneut erzeugt und gefiltert; nützlich, wenn die vollständigen Vorschlags-Objekte nicht herumkopiert werden sollen:

curl -sS -X POST "$COMPASS_URL/import/claude-md/apply" \
  -H "X-Compass-Key: $COMPASS_KEY_RW" -H "Content-Type: application/json" \
  -d '{"auswahl": ["always-encrypt-data-at-rest"], "text": "## Security\n\n- Always encrypt data at rest.\n- Never log a password in plain text.\n"}'

items oder (auswahl + text) ist Pflicht — 422 ohne eines von beiden, 422 wenn auswahl einen Slug nennt, den die neu erzeugte Vorschau nicht enthält.

Die Antwort:

{
  "geschrieben": [{"sache": "always-encrypt-data-at-rest", "aussage_id": 101}],
  "unveraendert": [],
  "abgeloest": [],
  "konflikt": [],
  "quelle_ref_widerspruch": []
}
  • Die Sache wird angelegt (oder unverändert wiederverwendet, falls sie schon existiert — idempotent auf slug, wie bei POST /sachen).
  • quelle der Aussage ist "<Dateiname>:<Zeile>" — der Dateiname kommt aus dem quelle-Feld des Aufrufs, Standard je art: CLAUDE.md / AGENTS.md / cursor-rules.mdc; quelle_ref ist "import:<art>:<Dateiname>:<Zeile>".
  • Gilt schon derselbe Wortlaut, wird nichts geschrieben — der Slug wandert nach unveraendert. Das gilt für beide Wege, über die COMPASS das erkennt: denselben Vorschlag erneut anwenden (idempotent über quelle_ref) und denselben Regeltext unter anderer Herkunft.
  • Weicht der Wortlaut ab, wird die geltende Aussage abgelöst (nur bei art=claude-md, also einer regel): die neue Aussage zeigt über ersetzt auf die alte, und der Eintrag erscheint in abgeloest: [{sache, aussage_id, loest_ab}]. Gelöscht wird nichts — die alte Aussage behält ihre Geltungsspanne und bleibt im Verlauf sichtbar. Dass es passieren wird, hat die Vorschau vorher als weicht_ab gesagt.
  • Der frühere Weg (nichts schreiben, konflikt melden) galt bis zum 24.09.2026. konflikt bleibt für den Wettlauf: schreibt jemand anderes zwischen Vorschau und Anwenden eine geltende Regel zur selben Sache, wird gemeldet statt geraten. Derselbe quelle_ref mit abweichendem regel_text schreibt ebenfalls nichts; stattdessen ein Eintrag in quelle_ref_widerspruch: [{sache, quelle_ref, bestehend}] — nie ein stilles Überschreiben.
  • Trägt die Sache bereits eine geltende Regel unter einem anderen quelle_ref (z. B. eine andere Zeile derselben Datei, oder ein früheres manuelles POST /aussagen), wird sie nicht ersetzt: der Eintrag landet in konflikt: [{sache, grund, geltende}], nichts wird geschrieben. Das gilt nur für art=claude-md — nur regel-Aussagen unterliegen der Ein-Regel-Invariante.

Zeilenschutz gilt wie überall: jede Zeile gehört dem Mandanten des präsentierten Schlüssels; ein anderer Mandant sieht nichts davon, auch nicht bei identischem sache-Slug (Primärschlüssel ist (kunde_slug, slug)).

AGENTS.md oder Cursor-Regeln importieren

Dieselben zwei Aufrufe, mit art=agents-md bzw. art=cursor-rules:

curl -sS -X POST "$COMPASS_URL/import/claude-md" \
  -H "X-Compass-Key: $COMPASS_KEY_RO" \
  -F "file=@AGENTS.md" -F "art=agents-md"

curl -sS -X POST "$COMPASS_URL/import/claude-md/apply" \
  -H "X-Compass-Key: $COMPASS_KEY_RW" -H "Content-Type: application/json" \
  -d '{"art": "agents-md", "items": [ ...aus der Vorschau... ]}'

Die hier geschriebene Aussage ist ein fakt (herkunft=gesagt), keine regel — AGENTS.md und .cursor/rules/*.mdc erklären sich beide selbst als nachrangig zu CLAUDE.md („Warum fakt, nicht regel"), und eine zweite regel auf derselben Sache würde mit der Ein-Regel-Invariante kollidieren und abgewiesen werden. Das heißt: ein art=agents-md-Import neben einer bestehenden art=claude-md-Regel auf derselben Sache kollidiert nie — es sind unterschiedliche Aussage-Arten. Für .cursor/rules/*.mdc beide Aufrufe je Datei einmal ausführen.

Entscheidungen statt Überschriften importieren

Wer ein Protokoll datierter Entscheidungen hat statt eines Regeldokuments, ist bei POST /import/decisions besser aufgehoben: {items: [{z, quote, context, decided_at, sache?, ersetzt_z?}]}, idempotent auf die z-Nummer, jeder Widerspruch landet in einer widerspruch-Liste statt still übersprungen oder überschrieben zu werden. Eine Entscheidung ohne (bekannte) sache wird trotzdem gespeichert — als Ereignis statt als Aussage, damit aus dem Protokoll nichts verloren geht. Das ist eine bruecke-Route — ein bruecke-Schlüssel funktioniert hier, obwohl er sonst fast nichts lesen oder schreiben kann.

Assistent-Vorschau (Status: vorbereitet)

Für Text, den der deterministische Scanner oben NICHT erkennt (Prosa ohne Imperativ-Marker, Sätze, die eine Vorgabe beschreiben statt sie zu befehlen), gibt es einen zweiten, optionalen Weg: POST /assistent/vorschlaege {text, art?: claude-md|agents-md|cursor-rules|prosa}. Er ruft zuerst denselben Scanner wie /import/claude-md auf — alles, was der schon erkennt, bekommt keinen Modellaufruf — und schickt nur den Rest an ein Sprachmodell, sofern einer konfiguriert ist.

Dieser Weg ist heute vorbereitet, nicht standardmäßig aktiv. Die Anbieter-Wahl ist bewusst offen: Anthropic (ein aktuelles Haiku-Modell) ODER ein lokales, OpenAI-kompatibles Modell (z. B. Ollama) — Standard ist aus. GET /assistent/status → {anbieter, bereit, grund, verbrauch_heute, deckel} zeigt, ob überhaupt ein Anbieter läuft; ohne Konfiguration ist bereit=false mit einem Klartext-Grund (z. B. „COMPASS_MODELL_ANBIETER ist 'aus' (Standard)"), POST /assistent/vorschlaege antwortet dann mit 503, sofern der Scanner nicht schon alles abgedeckt hat — deckt er alles ab, bleibt die Antwort 200, auch ohne Anbieter.

Jeder Modell-Vorschlag trägt zusätzlich woertlich (nur true, wenn regel_text wirklich als Teilstring im Original vorkommt — serverseitig geprüft, nie ungeprüft vom Modell übernommen), optional bereich und optional pruefweg: {modul, parameter} (nur mit einem der drei tatsächlich vorhandenen Prüfweg-Module github_ruleset/ci_status/konfig_wert). Auch dieser Weg schreibt nichts — die Übernahme läuft über denselben POST /import/claude-md/apply wie oben, ein Vorschlag ist absichtlich so geformt, dass er sich 1:1 als items-Eintrag eignet (die Zusatzfelder vorher weglassen). Ein Tagesdeckel je Kunde (COMPASS_MODELL_DECKEL_AUFRUFE_TAG/ COMPASS_MODELL_DECKEL_TOKEN_TAG, Standard 200 Aufrufe/400000 Token) verhindert, dass ein aktivierter Anbieter unbemerkt übers Budget läuft — überschritten → 429. Die genaue Vorschlagsform steht in der REST-Referenz unter „Assistent".

Was diese Anleitung nicht abdeckt

Für keinen der Import-Wege gibt es ein MCP-Werkzeug — alle vier (/import/claude-md, /import/claude-md/apply, /import/decisions, /assistent/vorschlaege) sind heute reines REST.