Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ec9c5e3
KI-Vorschläge: Eigenname statt Spitzname, keine Doppelten über Aliase
leonatwork Oct 12, 2026
f9ea39b
Figuren zusammenführen, Gruppen in der App anlegen, Spitznamen-Feld
leonatwork Oct 12, 2026
6615890
Plan: Figuren, Namen und Gruppen (Phasen 1–9)
leonatwork Oct 12, 2026
f33f3bf
Editor-Blase: bleibt im Bild, Escape, Tastatur, Fokus-Modus (Phase 1)
leonatwork Oct 12, 2026
c128330
Sperrliste gegen private Manuskriptbegriffe, neutraler Azure-Endpunkt…
leonatwork Oct 12, 2026
aa56bf4
Namen zuordnen: Blase „Figur“, KI-Vorschläge als Spitznamen (Phase 2)
leonatwork Oct 12, 2026
633770b
Stränge: Haupt-/Nebenhandlung, KI-Vorschläge, lose Enden als Stränge
leonatwork Oct 12, 2026
08cef5d
Namens-Hinweise: Spitzname vor dem Namen, ähnliche Namen, ruhende Fig…
leonatwork Oct 12, 2026
8b00382
Statisten als dritte Rolle (Phase 4, F-14)
leonatwork Oct 12, 2026
062fbe7
Funktionsumfang: Einrichtungs-Assistent, Presets je Schreibertyp, No-…
leonatwork Oct 12, 2026
6913e95
Gruppen: mehrere pro Figur, Rollen in Gruppen (Phase 5, A-05/A-06)
leonatwork Oct 12, 2026
3ddabb7
Besetzungsübersicht Figuren × Kapitel (Phase 6, F-15)
leonatwork Oct 12, 2026
c867360
Spitznamen als Chips mit Kontext (Phase 7, F-16)
leonatwork Oct 12, 2026
c53192e
Personenverzeichnis als Abgabe-Format (Phase 8)
leonatwork Oct 12, 2026
16dbccf
Figurenfakten aus dem Text (Phase 9, F-17) und Doku
leonatwork Oct 12, 2026
46f3b0f
Beispielnamen in Tests und Doku neutral (Henrik/Hendrik)
leonatwork Oct 12, 2026
6f2ac4a
KI: dritter Anbieter „OpenAI-kompatibel“ (Ollama, LM Studio, OpenAI, …
leonatwork Oct 12, 2026
624b499
Merge PR-Branch claude/akte-planen-story-threads-41392a
leonatwork Oct 12, 2026
02a68c5
Merge PR-Branch claude/statisten-handling-b9afbc
leonatwork Oct 12, 2026
ea4c0a6
Merge PR-Branch claude/local-ai-compatibility-63acdc
leonatwork Oct 12, 2026
a888abc
Merge PR-Branch claude/funktionsprofile-onboarding
leonatwork Oct 12, 2026
6d03104
Kosten „keine Angabe“ auch bei Figurenfakten und Strang-Vorschlägen (…
leonatwork Oct 12, 2026
6e90bb3
Strang-Vorschläge und Figurenfakten folgen dem Funktionsumfang (KI-Er…
leonatwork Oct 12, 2026
e880dfd
Leser-Simulation: alle Simulationen speichern, erledigen/löschen, Fra…
leonatwork Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .githooks/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
# Sperrliste auch für die Commit-Nachricht (scripts/sperrliste.mjs).
exec node scripts/sperrliste.mjs --nachricht "$1"
3 changes: 3 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
# Sperrliste: keine Begriffe aus privaten Manuskripten committen (scripts/sperrliste.mjs).
exec node scripts/sperrliste.mjs
3 changes: 3 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
# Sperrliste: Branch-Namen und alle neuen Commits vor dem Push prüfen (scripts/sperrliste.mjs).
exec node scripts/sperrliste.mjs --push
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Lokale Mac-App (Tauri 2 + React/TypeScript) zum Schreiben und Planen von Romanen
- `core/` ist reines TypeScript ohne DOM und ohne Tauri; dort liegt die testbare Logik (Vitest).
- Die KI schreibt nie Text: `core/ki` darf nur `ProjektLeser` nutzen, nie `ProjektDateisystem`.
- Releases: Tag `v*` → `.github/workflows/release.yml`, Ablauf in `docs/release.md`; Version mit `node scripts/version.mjs <x.y.z>` setzen.
- Keine Inhalte aus privaten Manuskripten ins Repo; Beispiele nur aus dem Demo-Roman in `fixtures/`.
- Keine Inhalte aus privaten Manuskripten ins Repo; Beispiele nur aus dem Demo-Roman in `fixtures/`. Die Git-Hooks in `.githooks/` (aktiv nach `npm install`) blockieren Commits und Pushes mit Begriffen aus `~/.config/plotwerk/sperrliste.txt`; `npm run sperrliste` prüft das ganze Repo.
- Rust liegt unter `/opt/homebrew/opt/rustup/bin` (vor Cargo-Befehlen `export PATH="/opt/homebrew/opt/rustup/bin:$PATH"`).

## Befehle
Expand Down
5 changes: 3 additions & 2 deletions core/abgabe/expose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/
import { AlignmentType, Document, HeadingLevel, HighlightColor, LineRuleType, Packer, Paragraph, TextRun } from 'docx';
import { buchSprache, fmt, localeVon, texteJe, type Sprache } from '../i18n';
import { nachRolle } from '../figuren/namen';
import type { Id, ProjektDaten } from '../model/types';
import { wortzahl } from '../text';
import { normseitenZaehlen } from './normseite';
Expand Down Expand Up @@ -162,8 +163,8 @@ export function exposeGeruest(daten: ProjektDaten, optionen: ExposeOptionen): Ex

bloecke.push({ art: 'ueberschrift', ebene: 1, text: t.figuren });
const figuren = Object.values(daten.figuren)
.filter((f) => optionen.nebenfiguren !== false || f.rolle === 'haupt')
.sort((a, b) => (a.rolle === b.rolle ? a.name.localeCompare(b.name, sprache) : a.rolle === 'haupt' ? -1 : 1));
.filter((f) => f.rolle !== 'statist' && (optionen.nebenfiguren !== false || f.rolle === 'haupt'))
.sort(nachRolle((a, b) => a.name.localeCompare(b.name, sprache)));
if (figuren.length === 0) bloecke.push({ art: 'absatz', text: [P(t.figurenFrage)] });
for (const f of figuren) {
bloecke.push({ art: 'punkt', text: [F(f.name), T(' – '), f.kurz?.trim() ? T(f.kurz.trim()) : P(t.kurzbeschreibung)] });
Expand Down
1 change: 1 addition & 0 deletions core/abgabe/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export {
type ExposeOptionen,
type ExposeStueck,
} from './expose';
export { personenAuswahl, personenKapitel, personenverzeichnis, type PersonenOptionen } from './personen';
export {
exportiereLektoratsmappe,
HINWEIS_TYP_NAME,
Expand Down
58 changes: 58 additions & 0 deletions core/abgabe/personen.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { beforeAll, describe, expect, it } from 'vitest';
import { demoFs } from '../../fixtures/demoProjekt';
import { berechneIndex } from '../index/index';
import type { ProjektDaten } from '../model/types';
import { ladeProjekt } from '../projekt';
import { exposeMarkdown } from './expose';
import { personenAuswahl, personenKapitel, personenverzeichnis } from './personen';

let daten: ProjektDaten;
let erst: Record<string, string>;
beforeAll(async () => {
daten = await ladeProjekt(demoFs());
erst = berechneIndex(daten).erstesVorkommen;
});

describe('Personenverzeichnis', () => {
it('nach Gruppen: Hauptgruppen als Abschnitte, Rest unter „Weitere Figuren“; nie private Felder', () => {
const d: ProjektDaten = {
...daten,
figuren: { ...daten.figuren, ole: { ...daten.figuren.ole!, aliase: ['Fenster-Ole'], wahrheit: 'GEHEIM', notizen: 'PRIVAT' } },
};
const md = exposeMarkdown(personenverzeichnis(d, erst, { gliederung: 'gruppen' }));
expect(md).toMatch(/^# Personen/);
expect(md).toContain('## Nordhaven');
expect(md).toContain('## Weitere Figuren');
expect(md).toContain('**Ole**');
expect(md).toContain('auch „Fenster-Ole“');
expect(md).not.toContain('GEHEIM');
expect(md).not.toContain('PRIVAT');
// bewusst vage Figuren bleiben draußen, geplante (noch nicht im Text) auch
const vage = Object.values(d.figuren).find((f) => f.existenz === 'bewusst_vage');
if (vage) expect(md).not.toContain(vage.name);
});

it('nach Rolle; Spitznamen abschaltbar; Statisten nur auf Wunsch', () => {
const d: ProjektDaten = { ...daten, figuren: { ...daten.figuren, malte: { ...daten.figuren.malte!, rolle: 'statist', aliase: ['M.'] } } };
const ohne = exposeMarkdown(personenverzeichnis(d, erst, { gliederung: 'rolle', spitznamen: false }));
expect(ohne).toContain('## Hauptfiguren');
expect(ohne).not.toContain('Malte');
const mit = exposeMarkdown(personenverzeichnis(d, erst, { gliederung: 'rolle', statisten: true, spitznamen: false }));
expect(mit).toContain('**Malte**');
expect(mit).not.toContain('„M.“');
});

it('nur bis Kapitel X: keine Figuren, die erst später auftauchen', () => {
const erstes = daten.projekt.kapitel[0]!;
const bis = personenAuswahl(daten, erst, { gliederung: 'rolle', bisKapitel: erstes });
expect(bis.every((f) => erst[f.id] === erstes)).toBe(true);
expect(bis.length).toBeLessThan(personenAuswahl(daten, erst, { gliederung: 'rolle' }).length);
});

it('als Vorspann-Kapitel (Markdown mit fett/kursiv)', () => {
const k = personenKapitel(personenverzeichnis(daten, erst, { gliederung: 'rolle' }));
expect(k.titel).toBe('Personen');
expect(k.text).toContain('## Hauptfiguren');
expect(k.text).toMatch(/\*\*Paul\*\*/);
});
});
109 changes: 109 additions & 0 deletions core/abgabe/personen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* Personenverzeichnis (G7 Abgabe): Figuren nach Gruppen oder Rolle, je Eintrag Name, Spitznamen, Kurzbeschreibung
* und Rolle in der Gruppe. Liefert dieselben Blöcke wie das Exposé (exposeDocx/exposeMarkdown) und als Markdown
* für einen Vorspann vor EPUB und Normseite. Private Felder (`wahrheit`, `notizen`, Planfelder) kommen nie hinein.
*/
import { fmt, texteJe, type Sprache } from '../i18n';
import type { Figur, Id, Kapitel, ProjektDaten } from '../model/types';
import { nachRolle } from '../figuren/namen';
import type { ExposeBlock, ExposeStueck } from './expose';

const T = texteJe(
{
titel: 'Personen',
haupt: 'Hauptfiguren',
neben: 'Nebenfiguren',
statist: 'Weitere Figuren',
ohneGruppe: 'Weitere Figuren',
auch: 'auch {namen}',
zitat: '„{text}“',
keine: 'Noch keine Figuren.',
},
{
titel: 'Characters',
haupt: 'Main characters',
neben: 'Supporting characters',
statist: 'Other characters',
ohneGruppe: 'Other characters',
auch: 'also {namen}',
zitat: '“{text}”',
keine: 'No characters yet.',
},
);

export interface PersonenOptionen {
/** nach Gruppen (Hauptgruppe) oder nach Rolle */
gliederung: 'gruppen' | 'rolle';
/** Statisten aufnehmen (Standard: nein) */
statisten?: boolean;
/** Spitznamen nennen (Standard: ja) */
spitznamen?: boolean;
/** nur Figuren, die bis zu diesem Kapitel im Text vorkommen (kein Spoiler); fehlt = alle im Text */
bisKapitel?: Id;
/** auch Figuren, die noch nicht im Text vorkommen (Standard: nein) */
geplante?: boolean;
/** „bewusst vage“ Figuren aufnehmen (Standard: nein – sie sollen offen bleiben) */
vage?: boolean;
}

/** Figuren für das Verzeichnis (Filter nach Optionen), in Listenreihenfolge. */
export function personenAuswahl(daten: ProjektDaten, erstesVorkommen: Record<Id, Id>, optionen: PersonenOptionen): Figur[] {
const reihe = daten.projekt.kapitel;
const bis = optionen.bisKapitel ? reihe.indexOf(optionen.bisKapitel) : reihe.length - 1;
return Object.values(daten.figuren)
.filter((f) => optionen.statisten || f.rolle !== 'statist')
.filter((f) => optionen.vage || f.existenz !== 'bewusst_vage')
.filter((f) => {
const erst = erstesVorkommen[f.id];
if (!erst) return !!optionen.geplante && !optionen.bisKapitel;
return reihe.indexOf(erst) <= bis;
})
.sort(nachRolle((a, b) => a.name.localeCompare(b.name, 'de')));
}

function eintrag(f: Figur, daten: ProjektDaten, optionen: PersonenOptionen, sprache: Sprache): ExposeStueck[] {
const t = T(sprache);
const teile: ExposeStueck[] = [{ text: f.name, art: 'fett' }];
const zusatz: string[] = [];
if (optionen.spitznamen !== false && f.aliase.length) zusatz.push(fmt(t.auch, { namen: f.aliase.map((a) => fmt(t.zitat, { text: a })).join(', ') }));
const rolle = f.gruppe ? daten.gruppen[f.gruppe]?.rollen?.[f.id] : undefined;
const beschreibung = [rolle, f.kurz?.trim()].filter(Boolean).join(', ');
if (beschreibung) teile.push({ text: ` – ${beschreibung}` });
if (zusatz.length) teile.push({ text: ` (${zusatz.join('; ')})`, art: 'kursiv' });
return teile;
}

/** Blöcke des Verzeichnisses (Überschrift, Abschnitte, Einträge als Aufzählung). */
export function personenverzeichnis(daten: ProjektDaten, erstesVorkommen: Record<Id, Id>, optionen: PersonenOptionen, sprache: Sprache = 'de'): ExposeBlock[] {
const t = T(sprache);
const figuren = personenAuswahl(daten, erstesVorkommen, optionen);
const bloecke: ExposeBlock[] = [{ art: 'titel', text: t.titel }];
if (!figuren.length) return [...bloecke, { art: 'absatz', text: [{ text: t.keine }] }];
const abschnitt = (titel: string, liste: Figur[]) => {
if (!liste.length) return;
bloecke.push({ art: 'ueberschrift', ebene: 1, text: titel });
for (const f of liste) bloecke.push({ art: 'punkt', text: eintrag(f, daten, optionen, sprache) });
};
if (optionen.gliederung === 'rolle') {
abschnitt(t.haupt, figuren.filter((f) => f.rolle === 'haupt'));
abschnitt(t.neben, figuren.filter((f) => f.rolle === 'neben'));
abschnitt(t.statist, figuren.filter((f) => f.rolle === 'statist'));
return bloecke;
}
const gruppen = Object.values(daten.gruppen).sort((a, b) => a.name.localeCompare(b.name, sprache));
for (const g of gruppen) abschnitt(g.name, figuren.filter((f) => f.gruppe === g.id));
abschnitt(t.ohneGruppe, figuren.filter((f) => !f.gruppe || !daten.gruppen[f.gruppe]));
return bloecke;
}

/** Als Kapitel für den Vorspann von EPUB und Normseite (Markdown wie im Manuskript: **fett**, *kursiv*). */
export function personenKapitel(bloecke: ExposeBlock[]): Kapitel {
const stuecke = (s: ExposeStueck[]) => s.map((x) => (x.art === 'fett' ? `**${x.text}**` : x.art === 'kursiv' ? ` *${x.text.trim()}*` : x.text)).join('');
const titel = bloecke.find((b) => b.art === 'titel');
const absaetze: string[] = [];
for (const b of bloecke) {
if (b.art === 'ueberschrift') absaetze.push(`## ${b.text}`);
else if (b.art === 'punkt' || b.art === 'absatz') absaetze.push(stuecke(b.text));
}
return { id: 'personenverzeichnis', titel: titel?.art === 'titel' ? titel.text : '', text: absaetze.join('\n\n') };
}
65 changes: 65 additions & 0 deletions core/figuren/besetzung.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* F-15 Besetzungsübersicht: Figuren × Kapitel. Je Zelle die wichtigste Art des Vorkommens (tritt auf > per
* Nachricht > erwähnt > im Text, Art offen) oder „plan“, wenn die Figur nur im Kapitelplan steht. Reine Ableitung
* aus dem Index, keine eigene Datei.
*/
import type { Erwaehnung, ErwaehnungsArt, Figur, Id, ProjektDaten } from '../model/types';
import { ROLLEN } from './namen';

export type BesetzungsZelle = ErwaehnungsArt | 'plan' | null;

export interface BesetzungsZeile {
figur: Figur;
/** je Kapitel (Reihenfolge wie `kapitel`) */
zellen: BesetzungsZelle[];
/** erste Erwähnung je Kapitel (zum Springen) */
erste: (Erwaehnung | null)[];
/** Kapitel mit Vorkommen im Text */
anzahl: number;
}

export interface Besetzung {
kapitel: Id[];
zeilen: BesetzungsZeile[];
/** Figuren mit Vorkommen im Text je Kapitel */
summen: number[];
}

/** Rang der Arten (gleiche Reihenfolge wie im Auftrittsstreifen der Figurenkarte). */
export const ART_RANG: Record<ErwaehnungsArt, number> = { auftritt: 4, nachricht: 3, erwaehnt: 2, unbestimmt: 1 };

export function besetzung(daten: ProjektDaten, erwaehnungen: Erwaehnung[]): Besetzung {
const kapitel = [...daten.projekt.kapitel];
const pos = new Map(kapitel.map((id, i) => [id, i]));
const figuren = Object.values(daten.figuren).sort(
(a, b) => ROLLEN.indexOf(a.rolle) - ROLLEN.indexOf(b.rolle) || a.name.localeCompare(b.name, 'de'),
);
const zeilen = new Map<Id, BesetzungsZeile>(
figuren.map((f) => [f.id, { figur: f, zellen: kapitel.map(() => null), erste: kapitel.map(() => null), anzahl: 0 }]),
);
for (const e of erwaehnungen) {
if (e.zielTyp !== 'figur') continue;
const z = zeilen.get(e.ziel);
const i = pos.get(e.kapitel);
if (!z || i === undefined) continue;
const alt = z.zellen[i];
if (!alt || alt === 'plan' || ART_RANG[e.art] > ART_RANG[alt]) z.zellen[i] = e.art;
if (!z.erste[i] || e.start < z.erste[i]!.start) z.erste[i] = e;
}
kapitel.forEach((id, i) => {
for (const f of daten.plaene[id]?.figuren ?? []) {
const z = zeilen.get(f);
if (z && !z.zellen[i]) z.zellen[i] = 'plan';
}
});
const summen = kapitel.map(() => 0);
for (const z of zeilen.values()) {
z.zellen.forEach((c, i) => {
if (c && c !== 'plan') {
z.anzahl += 1;
summen[i]! += 1;
}
});
}
return { kapitel, zeilen: [...zeilen.values()], summen };
}
75 changes: 75 additions & 0 deletions core/figuren/fakten.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { demoFs } from '../../fixtures/demoProjekt';
import type { FigurFakt, ProjektDaten } from '../model/types';
import { ladeProjekt, parseKiFakten, serializeKiFakten } from '../projekt';
import { faktenDerFigur, faktenHinweise, mischeFakten, setzeFaktStatus, werteVertraeglich, wertForm } from './index';

const SAAB = 'Dann nimmt er den Schlüssel und fährt mit seinem alten Saab 900 zum Lindenhof.';
const VOLVO = 'Ich steige aus dem Volvo, schlage die Tür zu und sehe ihm nach, bis er hinter den Lagerhallen verschwunden ist.';
const ORGELN = 'Vera erzählt von ihrer Werkstatt, in der sie alte Orgeln restauriert, Paul hört nur halb zu.';

const fakt = (figur: string, merkmal: string, wert: string, kapitel: string, text: string): FigurFakt => ({
schluessel: `fakt:${figur}|${merkmal.toLowerCase()}|${kapitel}/${text.slice(0, 20)}`,
figur,
merkmal,
wert,
kapitel,
zitat: { kapitel, text },
status: 'neu',
});

let daten: ProjektDaten;
beforeEach(async () => {
daten = await ladeProjekt(demoFs());
});

describe('Figurenfakten: Vergleich, Widersprüche, Planung (F-17)', () => {
it('Werte: Füllwörter fallen weg, enthalten = verträglich', () => {
expect(wertForm('seinem alten Saab 900')).toBe('seinem saab 900');
expect(werteVertraeglich('alter Saab 900', 'Saab')).toBe(true);
expect(werteVertraeglich('Saab', 'Volvo')).toBe(false);
expect(werteVertraeglich('Architekt', 'architekt')).toBe(true);
});

it('Widerspruch beim gleichen Merkmal → Hinweis „pruefen“ mit Figur-Bezug und beiden Zitaten', () => {
daten.kiFakten = { version: 1, eintraege: [fakt('paul', 'Auto', 'alter Saab 900', 'lotte', SAAB), fakt('paul', 'auto', 'Volvo', 'flut', VOLVO)] };
const g = faktenDerFigur(daten, 'paul');
expect(g).toHaveLength(1);
expect(g[0]!.widerspruch).toBe(true);
const h = faktenHinweise(daten).filter((x) => x.schluessel === 'fakt:widerspruch:paul|auto');
expect(h).toHaveLength(1);
expect(h[0]).toMatchObject({ typ: 'pruefen', plan: { figur: 'paul' } });
expect(h[0]!.zitate.map((z) => z.text)).toEqual([SAAB, VOLVO]);
});

it('Planabweichung: Planfeld gleichen Namens, kein Textwert passt', () => {
daten.figuren.vera = { ...daten.figuren.vera!, plan: { ...daten.figuren.vera!.plan, Beruf: 'Geigenbauerin' } };
daten.kiFakten = { version: 1, eintraege: [fakt('vera', 'Beruf', 'restauriert alte Orgeln', 'ebbe', ORGELN)] };
const g = faktenDerFigur(daten, 'vera')[0]!;
expect(g.plan).toEqual({ feld: 'Beruf', wert: 'Geigenbauerin', abweichend: true });
expect(faktenHinweise(daten).map((x) => x.schluessel)).toContain('fakt:plan:vera|beruf');
// passender Plan: kein Hinweis
daten.figuren.vera = { ...daten.figuren.vera!, plan: { ...daten.figuren.vera!.plan, Beruf: 'restauriert Orgeln' } };
expect(faktenHinweise(daten).filter((x) => x.schluessel.startsWith('fakt:plan:vera'))).toEqual([]);
});

it('verworfene und nicht mehr gültige Angaben zählen nicht; Mischen ersetzt nur die abgefragten Figuren', () => {
const a = fakt('paul', 'Auto', 'alter Saab 900', 'lotte', SAAB);
const b = fakt('paul', 'Auto', 'Volvo', 'flut', VOLVO);
const v = fakt('vera', 'Beruf', 'Orgeln', 'ebbe', ORGELN);
let ablage = mischeFakten(undefined, ['paul', 'vera'], [a, b, v]).ablage;
ablage = setzeFaktStatus(ablage, b.schluessel, 'verworfen');
daten.kiFakten = ablage;
expect(faktenDerFigur(daten, 'paul')[0]!.widerspruch).toBe(false);
// neuer Lauf nur für Paul: Verworfenes kommt nicht wieder, Vera bleibt
const zweiter = mischeFakten(ablage, ['paul'], [a, b]);
expect(zweiter.ablage.eintraege.filter((x) => x.figur === 'paul').map((x) => x.status).sort()).toEqual(['neu', 'verworfen']);
expect(zweiter.ablage.eintraege.some((x) => x.figur === 'vera')).toBe(true);
expect(zweiter.neu).toBe(0);
// Ablage übersteht Speichern
expect(parseKiFakten(serializeKiFakten(zweiter.ablage))).toEqual(zweiter.ablage);
// geänderter Text: Beleg nicht mehr gültig
daten.kapitel.lotte = { ...daten.kapitel.lotte!, text: 'Ganz anders.' };
expect(faktenDerFigur(daten, 'paul')).toEqual([]);
});
});
Loading
Loading