Phase 2.4 - Deterministische Konflikt-Vorpruefung: - src/ingest.ts: preCheckConflict(note, wiki) -> boolean Erkennt Verneinungen (kein, nicht, nie) + Wort-Ueberlappung - In src/cli/ingest.ts: vor LLM-Parse aufgerufen Wenn Konflikt erkannt + LLM hat nicht erkannt -> konflikt_hart=true, status=konflikt - 7 Tests: erkennt, ignoriert, Wortgrenzen, Verneinungs-Varianten Phase 2.5 - Inhaltliche Aehnlichkeit (Jaccard): - src/ingest.ts: jaccardSimilarity(a, b) -> 0-1 Deterministisch, keine externen Abhaengigkeiten - 5 Tests: identisch, verschieden, Ueberlappung, leer, Wortlaenge - 3 extractWords-Tests: lowercase, Satzzeichen, Unicode Edge-Case-Tests fuer Indexer: - tests/indexer-edge-cases.test.ts: 7 Tests - leerer Vault - Notiz ohne Frontmatter (Typ aus Pfad erkannt) - Unicode (Kaffee_AeOeUe) - tote Wikilinks - zirkulaere Wikilinks - Mehrfach-Wikilinks dedupliziert - Wikilink mit Anzeige-Text [[A|Alias]] - Konfig.md als SKIP_FILE (System-Datei, nicht indexieren) Phase 4.1 - Tiptap-Editor: - webapp/src/Editor.tsx: Tiptap + StarterKit Live-Preview (Split-View), Speichern-Button, Status-Indikator HTML<->Markdown via marked + turndown - webapp/src/EditorWithLoad.tsx: Lade/Speichere-Logik - GET /api/notes/<id> beim Oeffnen - PUT /api/notes/<id> beim Speichern - POST /api/sync manuell - webapp/src/notes-api.ts: API-Client - webapp/src/turndown.d.ts: Type-Shim Phase 4.2 - PWA-Manifest + Service Worker: - webapp/public/manifest.webmanifest: Standalone, Theme, Icons - webapp/public/icon-192.svg, icon-512.svg: SVG-Icons - webapp/public/service-worker.js: Cache-First, offline-faehig - webapp/index.html: Link auf manifest + Service-Worker-Registrierung Phase 4.3 - Sync-Trigger in PWA: - webapp/vite.config.ts: notesApiPlugin mit /api/notes/* und /api/sync Liest aus VAULT_PATH (Env-Var MINDOMAT_VAULT_PATH) Schreibt Notizen, ruft mindomat sync als Subprozess - Sync-Button in EditorWithLoad: manueller Trigger Im Production-Mode wuerde Background-Sync via Service Worker noetig Phase 5.1 - TWA-Konfiguration: - webapp/twa-manifest.json: Bubblewrap-Manifest mit Package-ID, Host, Fingerprints - webapp/public/.well-known/assetlinks.json: Digital Asset Links - webapp/TWA-BUILD.md: Vollstaendige Build-Anleitung (Bubblewrap, Play Store, Capacitor-Alternative) SPEC v2.0 + Plan v1.0: - Status-Uebersicht: alle 5 Phasen komplett - Tests: 50/50 gruen - Was-tut-Liste: PWA-Editor, TWA-Wrapper - Was-nicht-tut-Liste: kein iOS, kein Cloud-Sync, keine externen Suche-APIs - Plan v1.0: gleicher Status Verifiziert: - Tool: 50/50 Tests, build OK - Webapp: 99 Module, 1052 KB JS, 7.36s Build - Editor: Tiptap laedt/speichert Notizen via Vite-Plugin - PWA: Manifest + Service Worker + Icons vorhanden - TWA: Konfig + Build-Anleitung + assetlinks.json
93 lines
3.1 KiB
TypeScript
93 lines
3.1 KiB
TypeScript
// Tests fuer 2.4 (preCheckConflict) + 2.5 (jaccardSimilarity)
|
|
// Phase 2.4 + 2.5
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { preCheckConflict, jaccardSimilarity, extractWords } from '../src/ingest.js';
|
|
|
|
describe('preCheckConflict (2.4)', () => {
|
|
it('erkennt Konflikt: Notiz verneint, Wiki bejaht', () => {
|
|
const note = 'Ich trinke keinen Kaffee mehr.';
|
|
const wiki = 'Ich trinke jeden Morgen Kaffee.';
|
|
expect(preCheckConflict(note, wiki)).toBe(true);
|
|
});
|
|
|
|
it('kein Konflikt ohne Verneinung in Notiz', () => {
|
|
const note = 'Ich trinke Kaffee.';
|
|
const wiki = 'Ich trinke jeden Morgen Kaffee.';
|
|
expect(preCheckConflict(note, wiki)).toBe(false);
|
|
});
|
|
|
|
it('kein Konflikt bei geteilten Worten ohne Verneinung', () => {
|
|
const note = 'Kaffee am Morgen ist gut.';
|
|
const wiki = 'Kaffee ist lecker.';
|
|
expect(preCheckConflict(note, wiki)).toBe(false);
|
|
});
|
|
|
|
it('kein Konflikt wenn Verneinung, aber keine geteilten Wörter', () => {
|
|
const note = 'Ich mag kein Tennis.';
|
|
const wiki = 'Kaffee ist gut.';
|
|
expect(preCheckConflict(note, wiki)).toBe(false);
|
|
});
|
|
|
|
it('erkennt "nicht mehr" als Verneinung', () => {
|
|
const note = 'Ich rauche nicht mehr.';
|
|
const wiki = 'Ich rauche Zigaretten.';
|
|
expect(preCheckConflict(note, wiki)).toBe(true);
|
|
});
|
|
|
|
it('erkennt "nie" als Verneinung', () => {
|
|
const note = 'Ich war nie in Berlin.';
|
|
const wiki = 'Berlin ist die Hauptstadt.';
|
|
expect(preCheckConflict(note, wiki)).toBe(true);
|
|
});
|
|
|
|
it('Wortgrenzen beachtet: "KeinMann" wird nicht als Verneinung erkannt', () => {
|
|
const note = 'KeinMann kann das.';
|
|
const wiki = 'Ein Mann kann das.';
|
|
expect(preCheckConflict(note, wiki)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('jaccardSimilarity (2.5)', () => {
|
|
it('identische Texte ergeben 1.0', () => {
|
|
expect(jaccardSimilarity('Kaffee und Kuchen', 'Kaffee und Kuchen')).toBe(1.0);
|
|
});
|
|
|
|
it('voellig verschiedene Texte ergeben 0.0', () => {
|
|
expect(jaccardSimilarity('Kaffee trinken', 'Auto fahren')).toBe(0.0);
|
|
});
|
|
|
|
it('teilweise Ueberlappung ergeben Wert dazwischen', () => {
|
|
const a = 'Kaffee am Morgen mit Milch';
|
|
const b = 'Kaffee am Abend mit Zucker';
|
|
const sim = jaccardSimilarity(a, b);
|
|
expect(sim).toBeGreaterThan(0);
|
|
expect(sim).toBeLessThan(1);
|
|
});
|
|
|
|
it('leere Texte ergeben 0', () => {
|
|
expect(jaccardSimilarity('', '')).toBe(0);
|
|
expect(jaccardSimilarity('Kaffee', '')).toBe(0);
|
|
});
|
|
|
|
it('Wortlaenge-Mindestschwelle: kurze Wörter werden ignoriert', () => {
|
|
// 'a' und 'im' sind zu kurz (< 4 Zeichen)
|
|
const sim = jaccardSimilarity('a b c d', 'a b c d');
|
|
expect(sim).toBe(0); // keine Wörter >= 4 Zeichen
|
|
});
|
|
});
|
|
|
|
describe('extractWords', () => {
|
|
it('lowercase + >= 4 Zeichen', () => {
|
|
expect(extractWords('Kaffee und ich')).toEqual(['kaffee']);
|
|
});
|
|
|
|
it('Satzzeichen werden entfernt', () => {
|
|
expect(extractWords('Kaffee, Milch! Zucker.')).toEqual(['kaffee', 'milch', 'zucker']);
|
|
});
|
|
|
|
it('Unicode bleibt erhalten', () => {
|
|
expect(extractWords('Größere Übung mit Äpfeln')).toEqual(['größere', 'übung', 'äpfeln']);
|
|
});
|
|
});
|