Phase 3, erster Teil: Vorlagen-Bausteine und Schwärzung

Entschieden und festgehalten: jede Akte trägt Kurzvermerke und Dokumente, die
Fallakte zusätzlich ihre Abschnitte. Die Trennung aus docs/05 bleibt damit
bestehen.

Eine Vorlage ist jetzt eine geordnete Liste von Bausteinen. Manche nehmen eine
Eingabe entgegen, andere liefern festen Text mit Platzhaltern. Damit tragen
dieselbe Struktur ein Vorfallsbericht, der fast nur aus Eingaben besteht, und
ein Sonderarbeitsvertrag, der fast nur aus Paragraphen und Unterschriftenzeilen
besteht. Eine neue Dokumentart bleibt ein Datensatz, kein Deployment.

Der Schwärzungs-Parser gibt Segmente zurück, nicht den vollen Text mit einem
Sichtbarkeits-Flag: was der Client ausblenden müsste, hat er dann schon. Die
Balkenlänge wird auf ein Raster von acht Zeichen gerundet, sonst verrät sie,
wie lang der geschwärzte Name war.

Ein Test vergleicht den TypeScript-Parser gegen die SQL-Funktion
strip_redactions aus der Migration. Laufen die beiden auseinander, findet die
Suche einen Begriff, den die Ansicht verbirgt — und der Treffer allein verrät
ihn dann.
This commit is contained in:
Harlekin
2026-09-02 12:39:35 +02:00
parent cf4321a3bd
commit 4da89fcf1c
4 changed files with 388 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
// @vitest-environment node
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { createPrismaClient, type PrismaClient } from '@afis/db'
import { stripRedacted } from '@/lib/redaction'
/**
* Die Schwärzung wird an zwei Stellen entfernt: in TypeScript beim Rendern und
* in SQL, bevor der Suchvektor gefüllt wird. Laufen die beiden auseinander,
* findet die Suche einen Begriff, den die Ansicht verbirgt — und der Treffer
* allein verrät ihn dann.
*/
const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL
const suite = url ? describe : describe.skip
let db: PrismaClient
const PROBEN = [
'Der Hinweis kam von [[c4]]Wachmann Perreira, Sektor B[[/c4]] am 27.08.',
'[[c2]]zwei[[/c2]] und [[c5]]fünf[[/c5]] und [[c0]]offen[[/c0]]',
'ganz ohne Auszeichnung',
'mehrzeilig [[c3]]Zeile eins\nZeile zwei[[/c3]] Ende',
'unvollständige [[c3]]Auszeichnung ohne Ende',
]
suite('Schwärzung in TypeScript und SQL', () => {
beforeAll(() => {
db = createPrismaClient(url)
})
afterAll(async () => {
await db.$disconnect()
})
it('entfernt in beiden Sprachen dieselben Stellen', async () => {
for (const probe of PROBEN) {
const [row] = await db.$queryRaw<{ ergebnis: string }[]>`
SELECT strip_redactions(${probe}) AS ergebnis
`
const ausSql = row?.ergebnis ?? ''
const ausTs = stripRedacted(probe)
// Auf Wortebene vergleichen: SQL ersetzt durch ein Leerzeichen, der
// Parser ebenso, aber die umliegenden Leerzeichen können abweichen.
expect(ausSql.split(/\s+/).filter(Boolean)).toEqual(
ausTs.split(/\s+/).filter(Boolean),
)
}
})
it('lässt geschwärzte Begriffe in keiner der beiden Fassungen stehen', async () => {
const probe = PROBEN[0]!
const [row] = await db.$queryRaw<{ ergebnis: string }[]>`
SELECT strip_redactions(${probe}) AS ergebnis
`
expect(row?.ergebnis).not.toContain('Perreira')
expect(stripRedacted(probe)).not.toContain('Perreira')
})
})