Kanzlei-Paket Terminplaner: Fragen Q1 bis Q26

Abschnitt 4 mit den 26 Fragen (Frage, Fundstelle im Text, unsere Annahme,
Alternative). Jede Kennung der früheren Listen "Für die Rechtsprüfung offen"
(H1 bis C8) steht in der Fundstelle genau einer Frage oder in Abschnitt 6
mit Begründung; die Hinweise aus den Reviews (Pilot/Kulanz, Rückkehr nur
nach Abo-Ende, Erstattung bei Ablehnung, Löschung auf Weisung, vereinbarte
Fassung in K15, G02 bei kostenlosen Buchungen) sind in den passenden Fragen.

Tests: Fragen lückenlos Q1 bis Q26 mit vier Teilen, jede Frage verwiesen,
Q4 nennt News, § 356 Abs. 3 und Altkunden, Abdeckung der entfernten Listen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-05 05:31:57 +00:00
1 parent 6765b14feb
commit 7050b63a37
2 files changed
+400 -1

No files matched your search

+119
View File
@@ -139,6 +139,7 @@ function mailText(rel) {
lies(rel)
.replace(/<!--[\s\S]*?-->/g, ' ')
.replace(/<style[\s\S]*?<\/style>/g, ' ')
.replace(/<script[\s\S]*?<\/script>/g, ' ')
.replace(/<\/?(?:strong|b|em|i|u|span|a)(?:\s[^>]*)?>/g, '')
.replace(/<[^>]+>/g, ' ')
.replace(/&nbsp;/g, ' ')
@@ -195,6 +196,8 @@ function quellen() {
lies('docs/rechtliches/termine-hinweise.md'),
// Mailvorlage G02 (Hinweise zum Widerrufsrecht in der Bestätigung)
mailText('server/mail-templates/G02 Termin bestaetigt.html'),
// Datenschutzerklärung von WEBklar (Absatz zur Funktionsbestellung in § 7, zitiert in Q23)
mailText('public/datenschutz.html'),
// früherer Text des News-Hakens (Korrekturvermerk in der Spezifikation)
lies('docs/superpowers/specs/2026-09-30-funktionen-news-design.md'),
// Hinweise im Kundenportal und Storno-Vorschläge der Vorlagen
@@ -377,3 +380,119 @@ test('paket_konsistenz_abo_ende: LB Nr. 8, AVV Nr. 9, 6c und K22 nennen 30 Tage
const drei = norm(abschnitt(2).split('### 2.3 ')[1].split('### 2.4 ')[0])
assert.ok(drei.includes('30 Tage') && drei.includes('Rückkehr') && drei.includes('spätestens am folgenden Tag'))
})
// ------------------------------------------------------------------------------------------------ Fragen Q1 bis Q26 (Task F15b)
const FRAGE_TEILE = ['**Frage.**', '**Fundstelle im Text.**', '**Unsere Annahme.**', '**Alternative.**']
/** Fragen aus Abschnitt 4: [{ nr, titel, text, teile: { frage, fundstelle, annahme, alternative } }] in der Reihenfolge des Pakets. */
function fragen() {
const text = abschnitt(4)
const treffer = [...text.matchAll(/^### Q(\d+): (.+)$/gm)]
return treffer.map((m, i) => {
const ende = i + 1 < treffer.length ? treffer[i + 1].index : text.length
const rumpf = text.slice(m.index, ende)
const stellen = FRAGE_TEILE.map((marke) => rumpf.indexOf(marke))
const teile = {}
;['frage', 'fundstelle', 'annahme', 'alternative'].forEach((name, k) => {
if (stellen[k] < 0) return
const bis = k + 1 < stellen.length ? stellen[k + 1] : rumpf.length
teile[name] = rumpf.slice(stellen[k] + FRAGE_TEILE[k].length, bis).trim()
})
return { nr: Number(m[1]), titel: m[2], text: rumpf, teile, stellen }
})
}
test('paket_marker_und_fragen: Q1 bis Q26 lückenlos, je vier Teile, jede Frage verwiesen, jeder Marker hat seine Frage', () => {
const alle = fragen()
assert.deepEqual(alle.map((f) => f.nr), Array.from({ length: 26 }, (_, i) => i + 1), 'lückenlos Q1 bis Q26 in dieser Reihenfolge')
assert.ok(!abschnitt(4).includes('Task F15b'), 'der Platzhalter von F15a ist ersetzt')
for (const f of alle) {
assert.ok(f.titel.trim().length >= 5, `Q${f.nr}: Titel`)
assert.ok(f.stellen.every((p) => p >= 0) && f.stellen.every((p, k) => k === 0 || p > f.stellen[k - 1]), `Q${f.nr}: vier Teile in der Reihenfolge Frage, Fundstelle, Annahme, Alternative`)
for (const [name, inhalt] of Object.entries(f.teile)) assert.ok(inhalt.length >= 20, `Q${f.nr}: ${name} ist leer`)
assert.ok(!/Task F15|TODO|\?\?\?/.test(f.text), `Q${f.nr}: kein Platzhalter`)
}
// jeder Marker nennt eine vorhandene Frage, und jede Frage wird mindestens einmal verwiesen
const verwiesen = new Set(marker(PAKET))
for (const q of verwiesen) assert.ok(q >= 1 && q <= 26 && alle.some((f) => f.nr === q), `⚖ Q${q} ohne ### Q${q}`)
for (const f of alle) assert.ok(verwiesen.has(f.nr), `Q${f.nr} wird nirgends mit ⚖ verwiesen`)
// Q4 (D7): News-Altkunden
const q4 = alle[3]
for (const wort of ['News', '§ 356 Abs. 3', 'Altkunden']) assert.ok(q4.text.includes(wort), `Q4 nennt „${wort}“`)
assert.ok(q4.teile.frage.includes('[Anzahl'), 'Q4: die Zahl der Fälle liefert der Inhaber')
})
/** Zeilen der Zuordnungstabelle in Abschnitt 6.3: Kennung -> { punkt, wo }. */
function zuordnung() {
const text = abschnitt(6).split('### 6.3 ')[1]
assert.ok(text, '6.3')
const zeilen = text.split('\n').filter((z) => /^\| [HABC]\d /.test(z))
const tabelle = {}
for (const z of zeilen) {
const zellen = z.replace(/^\|\s*|\s*\|$/g, '').split(' | ')
assert.equal(zellen.length, 3, z)
assert.ok(!(zellen[0] in tabelle), `Kennung doppelt: ${zellen[0]}`)
tabelle[zellen[0]] = { punkt: zellen[1], wo: zellen[2] }
}
return tabelle
}
test('paket_offene_punkte_abgedeckt: jeder Punkt der entfernten Listen steht in einer Frage oder hat in Abschnitt 6 einen Grund', () => {
const erwartet = [
...[1, 2, 3, 4].map((n) => `H${n}`),
...[1, 2, 3, 4, 5, 6].map((n) => `A${n}`),
...[1, 2, 3, 4, 5, 6].map((n) => `B${n}`),
...[1, 2, 3, 4, 5, 6, 7, 8].map((n) => `C${n}`),
]
const tabelle = zuordnung()
assert.deepEqual(Object.keys(tabelle), erwartet, 'jede Kennung genau einmal, in dieser Reihenfolge')
const alle = fragen()
for (const kennung of erwartet) {
const { punkt, wo } = tabelle[kennung]
assert.ok(punkt.length >= 10, `${kennung}: Punkt benannt`)
const frage = /^Q(\d+)$/.exec(wo)
if (frage) {
// Zuordnung zu einer Frage: die Kennung steht in der „Fundstelle im Text“ genau dieser Frage
const f = alle.find((x) => x.nr === Number(frage[1]))
assert.ok(f, `${kennung}: Frage ${wo} fehlt`)
assert.ok(new RegExp(`\\b${kennung}\\b`).test(f.teile.fundstelle), `${kennung} steht nicht in der Fundstelle von ${wo}`)
} else {
// sonst nur mit Begründung (hier) und nur die drei dokumentierten Fälle
assert.ok(wo.startsWith('hier: ') && wo.length > 40, `${kennung}: Grund fehlt`)
assert.ok(['B3', 'C5', 'C6'].includes(kennung), `${kennung} muss in einer Frage stehen`)
for (const f of alle) assert.ok(!new RegExp(`\\b${kennung}\\b`).test(f.teile.fundstelle), `${kennung} steht schon in Q${f.nr}`)
}
}
// jede Kennung in den Fragen ist in der Tabelle bekannt
for (const f of alle) {
for (const m of f.teile.fundstelle.matchAll(/\b([HABC]\d)\b/g)) assert.ok(erwartet.includes(m[1]), `Q${f.nr}: unbekannte Kennung ${m[1]}`)
}
})
test('paket_offene_punkte_abgedeckt: die Hinweise der Reviews (Kanzlei-Quellen) stehen in der passenden Frage', () => {
const alle = fragen()
const frage = (n) => alle[n - 1].text
const hat = (n, ...woerter) => { for (const w of woerter) assert.ok(frage(n).includes(w), `Q${n} nennt „${w}“`) }
hat(1, 'Verbraucher', 'Unternehmer')
hat(2, 'Zahlungseingang', 'K15')
hat(3, '§ 356 Abs. 5 Nr. 2', '§ 357a Abs. 2', 'dauerhaften Datenträger')
hat(5, 'Gesamtpreis', 'Arbeitsblatt', 'Stundenaufwand')
hat(6, 'abo_ende_vor_einrichtung', 'abo_ende', 'Nr. 8 Abs. 4', '§ 307')
hat(7, 'unlockedByPurchase', 'ausgenommen', 'Pilot', 'Kulanz')
hat(8, '§ 309 Nr. 7', 'Höchstbetrag', 'Einstellung')
hat(9, 'disabledAt', 'Terminanfragen', 'Rückkehr', 'Sicherung')
hat(10, 'unverzüglich', 'Absagegrund', 'Zählungen', 'Drittländer', 'Kalender')
hat(11, 'Erstattung', 'ablehnt', 'Art. 9')
hat(12, 'Lieferkosten', 'Gesetzlichkeitsfiktion')
hat(14, '§ 312j Abs. 3')
hat(16, 'G02', '§ 307', 'kostenlosen')
hat(19, '§ 19 UStG', '· 149 €')
hat(20, 'vereinbarten Fassung', 'Art. 246a § 1')
hat(21, 'Textform', 'K16', 'Art. 28 Abs. 9')
hat(22, 'BFSG')
hat(23, 'drei Jahre', 'Browserkennung')
hat(24, '90 Tage')
hat(25, 'Mindestlaufzeit')
hat(26, 'Hash', 'Archiv', 'Haltezeit')
})