# Workflow: Externe Quellen

Ziel: fremdes Material **satzweise** belegen, statt es einzufrieren. Ein Claim
zeigt über einen Satzanker auf genau einen Satz einer aufgeführten Quelle,
nachrechenbar über dessen SHA-256. Der Tresor bleibt offline antwortfähig,
weil der zitierte Satz mitliegt — eingefroren wird das Zitat, nicht das Repo.

Bauform und Begründung sind dieselben wie bei Bild-/PDF-Regionen
(`references/multimodal.md`): Der Ankertext bleibt untrusted Quelldatum und
ist **nie** Evidenz. Evidenz ist immer nur der kuratierte lokale Claim.

| | Bild/PDF | Externe Quelle |
|---|---|---|
| Quellklasse | `S-nnnn` | `X-nnnn` |
| Registry | `sources/derived/S-nnnn__media.json` | `sources/derived/X-nnnn__anchors.json` |
| Fundstelle im Claim | genau eine `R-nnnn` | genau eine `A-nnnn` |
| untrusted Feld | `regions[].text` | `anchors[].text` |

## Aufgeführt heißt nicht recherchiert

Die wichtigste Unterscheidung dieses Workflows:

* **Internetrecherche** — offene Suche, Suchmaschine, Links folgen, unbekannte
  Ziele. Der Tresor tut das nie. Es gibt keinen Codepfad, der ein Ziel aus
  einer Anfrage, einem Dokument oder einer Antwort übernimmt.
* **Aufgeführte Bezugsquelle** — steht namentlich mit Titel, Ziel, Trust und
  Rechten in `sources/EXTERN.md` und ist über `MANIFEST.sha256` gepinnt. Das
  ist Bestandsverwaltung, dieselbe Kategorie wie eine Zeile in
  `sources/REGISTER.md`.

Daraus folgt: **Eine aufgeführte Netzquelle wird behandelt wie eine interne —
auch dann, wenn im Umfeld ausdrücklich keine Internetrecherche erlaubt ist.**
Ein solches Verbot trifft die offene Suche, nicht das Nachlesen einer
namentlich aufgeführten, gepinnten Quelle des eigenen Bestands.

Damit das überprüfbar bleibt statt behauptet: nur `GET` unter dem
registrierten Präfix, jedes Redirect muss darunter bleiben, und jeder Abruf
steht mit URL, Prüfsumme, Bytezahl und Zeitpunkt in `log.md`.

## Zwei Allowlists, zwei Fragen

| Ort | im Manifest? | beantwortet |
|---|---|---|
| `sources/EXTERN.md` | **ja** | *Was* erreicht werden darf |
| `.vault-extern.json` | **nein** | *Wo* diese Maschine eine **lokale** Quelle findet |

Eine Netzquelle braucht keine Maschinenbindung: ihre URL ist auf jedem Host
dieselbe. Eine lokale Quelle braucht sie, weil ein absoluter Pfad nichts über
das Artefakt aussagt und im Manifest die Portabilität bräche.

Ohne Bindung ist eine lokale Quelle **ungebunden**: der Tresor antwortet
vollständig, jede Auflösung meldet fail-closed. Das ist bewusst **kein**
Validierungsfehler — sonst wäre jedes frisch entpackte Paket auf jedem neuen
Host rot. Fail-closed greift erst bei der Nutzung.

## Protokoll

1. **Aufführen.** Zeile in `sources/EXTERN.md` ergänzen: nächste freie
   `X-nnnn`, Titel, Art (`markdown-tree` oder `skillsafe-vault`), Ziel
   (https-Präfix oder `-`), Stand, Bindungsschlüssel, Trust, Rechte. Das ist
   ein bewusster Kurationsschritt mit Release — nie automatisch, nie zur
   Abfragezeit. Rechte und Trust klärt ein Mensch, nicht das Script.

2. **Binden** (nur lokale Quellen):
   `python3 scripts/vault.py extern bind X-0001 /absoluter/pfad`
   Die Wurzel muss absolut und frei von `..` sein, darf den Tresor weder
   enthalten noch in ihm liegen und keine andere gebundene Wurzel überlappen.
   Führt der Pfad über einen Symlink, wird er aufgelöst und in aufgelöster
   Form gespeichert — sichtbar, nicht still. `extern list` zeigt den Zustand aller Quellen.

3. **Sätze ansehen.**
   `python3 scripts/vault.py anchor-template X-0001 pfad/im/baum.md`
   Das Script zerlegt nach `satzsegmentierung/v1`, nummeriert und hasht. Das
   Modell **wählt** daraus aus; es zählt keine Zeilen und rechnet keine
   Prüfsummen.

4. **Ankerdatei schreiben.** Die gewählten Anker nach
   `sources/derived/X-nnnn__anchors.json` übernehmen, `locator` in
   menschenlesbare Form bringen (`Abschnitt "Anzeigepflicht", Satz 1`),
   `extractor` benennen und `verified: true` setzen. Ein Anker mit
   `suspicious_instruction: true` darf keinen Claim tragen — er bleibt
   markiertes Quelldatum.

5. **Claim kuratieren.** Auf der Wissensseite `externe_quellen: [X-0001]` ins
   Frontmatter, dann:
   `- **C-0401** [X-0001 | A-0004 | Wortlaut] Kuratierte, komprimierte Aussage.`
   `sources:` darf leer bleiben, wenn die Seite ausschließlich extern belegt
   ist. Ganz ohne Beleg bleibt sie unzulässig.
   `Beobachtung` bleibt Bild-/PDF-Quellen vorbehalten: ein zitierter Satz ist
   `Wortlaut`, eine Schlussfolgerung daraus `Auslegung`.

6. **Release.** `python3 scripts/vault.py release <stufe>`, danach `doctor`.

7. **Optional: Katalog für das Nachschlagen.**
   `python3 scripts/vault.py orchestrator-template X-0001 > sources/derived/X-0001__orchestrator.json`
   Das Script füllt alles Nachrechenbare (`path`, `sha256`, `sentence_count`,
   `token_count`, `top_tokens`); `title`, `summary` und `tags` sind
   Modellarbeit und bleiben leer im Gerüst. Genau diese Trennlinie ist später
   der Prüfpunkt. Für eine Netzquelle lässt sich kein fremder Baum ablaufen —
   ihr Katalog wird von Hand gepflegt und trägt **keine Prüfsummen**:
   `generated_from_sha256` und `documents[].sha256` müssen dort `null` sein.
   Ein Hash auf einen beweglichen Ref wäre nach dem nächsten fremden Commit
   unwahr. Bei einer lokalen Quelle sind beide dagegen Pflicht.

## Live nachschlagen

`python3 scripts/vault.py query --extern "<frage>" [--source X-nnnn]`

Ohne `--extern` verlässt **kein Byte** den Skill-Ordner, weder zur Platte noch
ins Netz. Das Flag macht am Aufruf sichtbar, dass hier bewusst eine Ausnahme
von Regel 2 (Vertrauen zur Abfragezeit) gemacht wird. Es gehört an das Ende
des Antworten-Workflows, nicht an den Anfang: erst `no_candidates`, dann die
vollständige Prüfung von `fallback.page_paths`, dann erst extern.

Das Ranking `extern-zweistufig/v1` arbeitet in zwei Stufen:

* **Stufe 1, Katalogtext** — entscheidet nur, welche Dokumente überhaupt
  geöffnet werden. Ein schwaches Signal.
* **Stufe 2, tatsächlicher Satzbestand** — wird gegen den **jetzt** gelesenen
  Text gerechnet, nicht gegen den Katalog. Das starke Signal.
* **Abdeckungsschwelle** — ein Dokument, in dem kein einziger Anfragebegriff
  als ganzes Token vorkommt, fällt raus, egal wie gut Stufe 1 aussah.

Warum zwei Stufen: Im Demo-Bestand liefert „Minderung Mangel Wohnung" in
Stufe 1 das Seerecht mit 78 Punkten **vor** dem Mietrecht mit 60 — die
Katalogtexte ähneln sich absichtlich. Stufe 2 dreht das auf 460 zu 152. Der
Katalogtext ist ein schwaches Signal, der Bestand dahinter das starke.

Abgeglichen wird ausschließlich an Tokengrenzen. „himmel" findet deshalb kein
„schimmel" — der Fehler ist strukturell ausgeschlossen, nicht per Konvention
vermieden.

## Ehrlich scheitern

Findet Stufe 2 nichts, kommt nichts zurück: `no_external_candidates`, leere
Trefferliste, und eine Meldung, die sagt, was zu tun ist (Fachbegriffe statt
Eigennamen). Kein bester Fehltreffer, kein alphabetischer Fallback. Ein
plausibel aussehender Fehltreffer ist teurer als kein Treffer, weil er nicht
als Fehler auffällt.

## Was bei der Abfrage sichtbar wird

Externe Treffer stehen im eigenen Block `external`, nie in `evidence` und nie
im `retrieval_fingerprint` — sonst hinge der bisher reproduzierbare Fingerprint
an fremder Verfügbarkeit. Jeder Treffer trägt redundant
`role: "external_pointer"` und `is_evidence: false`; der Unterschied zu einem
Claim darf sich nicht erst aus dem Kontext ergeben müssen. Zusätzlich sichtbar:
`score_catalog`, `score_inventory` und `coverage_percent` getrennt, damit der
Stufeneffekt ablesbar bleibt und nicht in einer Summe verschwindet.

Ein Satz mit Instruktionssignatur wird mit `text: null` und dem Signal
`suspicious_instruction` zurückgegeben — gemeldet, nie ausgeliefert.

## Drift

`doctor` prüft jeden Anker gegen die gebundene Quelle, **hash-first**: Findet
er den Satz-Hash an anderer Stelle, meldet er „verschoben, nicht gebrochen";
findet er ihn gar nicht, meldet er Drift. Beides ist **gelb, nie rot** und
blockiert keinen Release. Der Tresor kontrolliert die fremde Quelle nicht —
ihre Änderung ist keine Störung dieses Bestands, sondern Kuratierungsarbeit.

## Gemessen, nicht behauptet

Ein Orchestrator ohne Messung ist eine Behauptung. Gegen den mitgelieferten
Beispielbaum (`beispiel-extern/`, 4 Dokumente), Stand 2026-08-06:

| Kennzahl | Wert |
|---|---|
| Positivanfragen | 10 |
| Top-1-Trefferquote | 100 % |
| Negativanfragen (ohne richtige Antwort) | 5 |
| Fehltrefferquote | **0 %** |
| Abdeckung der richtigen Treffer | ≥ 50 % |
| Abdeckung der verworfenen Dokumente | 0 % |

Die Negativfälle sind der eigentliche Punkt. Ohne sie misst man nur, wie gern
ein System antwortet. Und der Nebenbefund taugt als Schwelle: Jeder richtige
Treffer hatte eine Abdeckung über null, jeder verworfene exakt null.

**Grenze dieser Zahl, ausdrücklich:** Der Beispielbaum hat vier Dokumente, und
Dokumente wie Anfragen stammen aus derselben Hand. Das misst, ob das Ranking
tut, was es soll — nicht, wie es sich auf einem gewachsenen Fremdbestand
schlägt. Deshalb steht die Zahl hier und in den Abnahmekriterien, aber nicht
auf der Landingpage. Wer den Tresor an ein echtes Korpus hängt, misst neu.

Der Testfall `test_external_routing_quality_stays_above_the_floor` hält eine
Untergrenze fest, damit die Qualität nicht still absacken kann.

## Grenzen

* **Der Anker ist kein Ingest.** Soll der Inhalt wirklich Bestand werden, geht
  er den regulären Weg über `references/befuellen.md`: Quarantäne,
  Registrierung als `S-nnnn`, Claim-Extraktion. Ein Anker ist eine Abkürzung
  um nichts davon.
* **Das Routing hängt an fremder Dokumentqualität.** Wer im fremden Baum
  schludert, bekommt schlechtere Treffer; kein Ranking heilt das.
* **Der fremde Baum kann eine `SKILL.md` enthalten**, die ein Agent nebenher
  lädt. Das kann `vault.py` nicht verhindern — es liegt außerhalb seiner
  Prozessgrenze und muss beim Binden bedacht werden.
* **Netzergebnisse sind nicht reproduzierbar.** Ein Socket-Timeout kann das
  Ergebnis formen. Deshalb tragen sie `live: true`, Abrufzeitpunkt und
  Cache-Alter, und sie bleiben aus dem lokalen Fingerprint heraus.
* **Kein Zugriffssystem.** Eine Bindung schafft keinen Zugriff, sie nutzt den,
  den das Betriebssystem ohnehin gewährt. Wer zwei Tresore hat, kann den
  sensibleren an den weniger sensiblen binden; das verhindert kein
  Mechanismus (AD-06). `doctor` warnt bei invertierter Scope-Richtung, mehr
  nicht.
