Phase 7 abgeschlossen: Suche mit gestuftem Index, Facetten, Palette

Der bisherige Suchvektor enthielt nur, was jeder sehen darf. Das war
leck-sicher und unbrauchbar zugleich: ab Stufe 1 fand die Suche nichts von
dem, was einen Fall ausmacht.

SearchIndex hält jetzt sechs Zeilen je Ziel, eine pro Freigabestufe. Die Zeile
zur Stufe N enthält alles, was ein Leser dieser Stufe sehen darf. Eine Suche
nimmt genau diese eine Zeile und kann deshalb nichts finden, was sie nicht
lesen darf — auch keinen höher eingestuften Eintrag in einer sichtbaren Akte.

Schwärzungen werden dabei stufenweise aufgelöst: strip_redactions_above
entfernt nur, was über der Stufe liegt. Sonst fände ein Leser der Stufe 4
einen Namen nicht, den er auf der Seite im Klartext sieht, und ein Leser der
Stufe 1 einen, den er dort nur als Balken sieht.

Die Suchbedingung ist nicht die Zugriffsprüfung: der Volltext liefert
Kandidaten, die Sichtbarkeit kommt aus visibleCaseWhere — derselben Funktion
wie in der Fallliste, und Treffer, Trefferzahl und Facettenzähler entstehen
gemeinsam in einer Abfrage. Eine in SQL nachgebaute Sichtbarkeitsregel wäre
die Sorte Duplikat, die auseinanderläuft.

Filter stehen in der Adresse. Damit ist jeder Zustand teilbar, der
Zurück-Knopf tut das Richtige, und eine gespeicherte Ansicht ist ein
Lesezeichen. Die mitgelieferten Ansichten stehen im Programm, weil sie von der
Gegenwart abhängen.

Strg+K öffnet die Kommandopalette. Im Grundgerüst steckt nur das
Tastaturereignis, die Palette selbst wird beim Öffnen geholt — das kostet die
anderen Seiten rund hundert Byte.

Offen geblieben: "Zuletzt geöffnet" als Ansicht. Dafür müsste jeder Aufruf
jeder Akte mitgeschrieben werden; das wäre eine Überwachungsfunktion, die
niemand bestellt hat. In TASKS vermerkt.

165 Tests im Web-Paket, 17 in der Datenbank, 7 im Worker, 34 e2e.
Grösste Route bei 119,7 kB gegen 120 erlaubte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Harlekin
2026-09-02 21:23:01 +02:00
co-authored by Claude Opus 5
parent 34a8b96dd3
commit 71c20f4b95
22 changed files with 1745 additions and 7 deletions
+70
View File
@@ -648,3 +648,73 @@ geändert da; das Aufräumen war von Hand nötig.
Die Lehre steht hier, weil sie sich wiederholen wird: **vor jedem neuen Modell, Enum oder Feld
im Schema nachsehen, ob es das schon gibt.** Das Schema ist über achthundert Zeilen lang, und
der Teil, den man gerade nicht bearbeitet, ist der, den man vergisst.
## Suche
### Ein Vektor je Freigabestufe
Der Suchvektor aus Phase 2 enthielt nur, was **jeder** sehen darf (`minClearance = 0`). Das
war leck-sicher und unbrauchbar zugleich: ab Stufe 1 fand die Suche nichts von dem, was einen
Fall ausmacht. Der Kommentar in der Migration sagte schon, dass Phase 7 getrennte Vektoren je
Stufe ergänzt — hier sind sie.
`SearchIndex` hält je Ziel sechs Zeilen, eine für jede Stufe. Die Zeile zur Stufe N enthält
alles, was ein Leser dieser Stufe sehen darf: Kopfdaten, Abschnitte und Einträge bis
einschliesslich N. Eine Suche nimmt **genau eine Zeile** — die zur eigenen Stufe — und kann
deshalb nichts finden, was sie nicht lesen darf.
Sechs Zeilen je Fall statt einer ist bei ein paar hundert Akten kein Thema, und die Abfrage
wird dadurch einfacher, nicht schwerer: kein `DISTINCT`, keine Aggregation über mehrere
Zeilen, ein einzelner Gleichheitsvergleich auf `minClearance`.
### Schwärzungen werden stufenweise aufgelöst
`strip_redactions` warf jede markierte Stelle weg. Für einen gestuften Index ist das falsch in
beide Richtungen: ein Leser der Stufe 4 fände einen Namen nicht, den er auf der Seite im
Klartext sieht, und ohne die Funktion fände ein Leser der Stufe 1 einen, den er dort nur als
Balken sieht.
`strip_redactions_above(text, stufe)` entfernt deshalb nur, was **über** der Stufe liegt, und
löst die übrigen Marker auf. Ein Durchgang je Stufe darüber, weil die Stufe als Ziffer im
Marker steht und `regexp_replace` nicht auf einen Fund hin verzweigen kann.
### Die Suchbedingung ist nicht die Zugriffsprüfung
Die Suche läuft in zwei Schritten, und der Unterschied ist der Punkt:
1. **Was passt** — eine Abfrage auf `SearchIndex`, die höchstens 500 Kennungen nach Rang
liefert.
2. **Was der Suchende sehen darf und wie viele es sind** — eine Abfrage mit
`visibleCaseWhere` und den Filtern. Treffer, Trefferzahl **und Facettenzähler** entstehen
dort gemeinsam.
Der erste Schritt ist eine Suchbedingung, kein Filter im Sinne von docs/08 — die
Zugriffsprüfung steht ausschliesslich im zweiten und stammt aus derselben Funktion wie in der
Fallliste. Es gibt keine zweite Fassung davon, und es sollte auch nie eine geben: eine in SQL
nachgebaute Sichtbarkeitsregel wäre genau die Sorte Duplikat, die irgendwann auseinanderläuft.
Die Begrenzung auf 500 kann bei sehr allgemeinen Begriffen etwas abschneiden. Die Oberfläche
sagt das dann ausdrücklich, statt eine vollständige Liste vorzutäuschen.
### Filter stehen in der Adresse, Ansichten sind Lesezeichen
Jeder Zustand der Suchseite ist eine Adresse. Damit ist er teilbar, der Zurück-Knopf tut das
Richtige, und eine gespeicherte Ansicht ist nichts weiter als eine gemerkte Adresse.
Die **mitgelieferten** Ansichten stehen im Programm und nicht in der Datenbank, weil sie von
der Gegenwart abhängen: „Wiedervorlage diese Woche" und „unbearbeitet seit 14 Tagen" wären als
gespeicherte Zeitpunkte schon am nächsten Tag falsch.
„Zuletzt geöffnet" aus docs/08 fehlt: dafür müsste jeder Aufruf jeder Akte mitgeschrieben
werden, und das Zugriffsprotokoll führt nur Stufe 3 aufwärts. Eine eigene Spur dafür anzulegen
wäre eine Überwachungsfunktion, die niemand bestellt hat — wenn sie gebraucht wird, sollte das
eine bewusste Entscheidung sein und keine Nebenwirkung.
### Die Kommandopalette wiegt nichts
Strg+K steckt in jeder Seite, und die Fallakte hat kein Bündel übrig. Deshalb ist der Teil im
Grundgerüst nur ein Tastaturereignis; die Palette selbst wird geholt, wenn jemand sie öffnet.
Das kostet die anderen Seiten rund hundert Byte.
Sie sucht bewusst schmal — Aktenzeichen, Titel, Namen, kein Volltext über Inhalte. Wer sie
öffnet, will springen, nicht recherchieren.