lean-path: Angefangenes liegt auf dem Pfad (SPEC §9, D61)

Eine Zugabe (`+`) fällt aus dem günstigsten Pfad — außer sie ist
realisiert und noch nicht erledigt (`[~]`/`[/]`). Angefangene Arbeit ist
die offene Front, und seit D46 zeigt der Pfad genau die; bisher führte
der Von-Station-zu-Station-Knopf nie dorthin, wo gearbeitet wird.
Erledigte Zugaben bleiben draußen: dort ist nichts mehr zu tun, und was
darunter offen blieb, ist mit ihnen zusammen entbehrlich (§3).

Dabei die fehlende Hälfte von D46 gefunden: SPEC §9 sagt seit jeher
„eine bereits realisierte Alternative gewinnt", umgesetzt war das aber
nur über die Kosten — und die sind allein bei `[x]`/`[^]` null. Ein
`[~]`-Zweig verlor damit gegen eine unangetastete billigere, obwohl die
XOR-Regel gerade ihn als die realisierte Alternative ausweist.
`chosenPool()` schränkt die Wahlmenge auf die realisierten ein; unter
mehreren entscheiden wieder die Kosten. Nebengewinn: eine entschiedene
Gruppe ist keine freie Variable mehr und verkleinert die Suche (D42).

10 neue Tests; Gegenprobe geführt (jede Regel einzeln herausgenommen
lässt genau die nach ihr benannten Zusicherungen fallen). Der
mitgelieferte Plan ändert sich nicht — er hat keine angefangene Zugabe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-24 14:11:38 +02:00
co-authored by Claude Opus 5
parent 3018db9f61
commit 36b78efa53
8 changed files with 232 additions and 25 deletions
+2
View File
@@ -19,6 +19,8 @@ reverse.
## 2026-08-24
- An optional node (`+`) joins the cheapest path while it is being worked on — started work is the open front
- Fix: a started alternative lost its `|`/`=` group to an untouched cheaper one, although the choice was already made
- A node written as an id alone (`- #US-123`) is now titled by that id, instead of being ignored
- A line ending in a space and `\` continues on the next line, so a long node no longer has to fit into one
- A star in the header opens What's new: the changes of the last few days, and a link per day that shows the nodes it touched in the diagram
+97
View File
@@ -4319,3 +4319,100 @@ Knoten.
`#`-Umschalter bekommt er **keine** `nid`-Spanne, der Nachbar `#auth: Backend`
schon. 337 Tests, davon 6 neue; der eine alte, der die frühere Regel festhielt,
ist umgeschrieben und benennt jetzt diese.
## D61 — Angefangenes liegt auf dem Pfad: Zugaben und Alternativen
Gemeldet an einem kleinen Baum:
```
- [ ] #fe
+ [~] #fe.rel: Relations bearbeitbar (S)
+ [?] #fe.more: weitere Features …
```
Erwartet war, dass der Pfad durch `#fe.rel` läuft — „auch wenn es optional
ist, weil das ja nun schon begonnen wurde". Er lief stattdessen durch `#fe`,
und die einzige Station war der Elternknoten.
**Die Regel dahinter stand seit D29 ohne Ausnahme da:** Optionale Knoten sind
*nie* nötig. Sie ist aus einem echten Fehler entstanden — der Pfad rechnete
jede Zugabe ins Minimum und **überschätzte** sich systematisch. Der umgekehrte
Fehler war dabei nie bedacht: angefangene Arbeit zu **unterschlagen**. Seit
D46 beantwortet der Pfad ohnehin nicht mehr „was hätte der Plan von vorn
gekostet?", sondern „was ist als Nächstes dran?" — und `[~]` ist das
Vorderste, was es gibt. Praktisch heißt das, dass der
Von-Station-zu-Station-Knopf (D47) einen nie dorthin führt, wo gerade
tatsächlich gearbeitet wird.
`+` ist eine Aussage über den **Plan** („entbehrlich"), der Status eine über
die **Tatsachen** („daran wird gearbeitet"). Die beiden Achsen bleiben
unabhängig (§3) — der Pfad ist ein Drittes und darf beide lesen.
**Entschieden: Eine Zugabe liegt auf dem Pfad, sobald sie realisiert (§3),
aber noch nicht erledigt (D46) ist** — also bei `[~]` und `[/]`.
**Die Formulierung ist der eigentliche Fund.** Mein erster Vorschlag war die
volle Schwelle „realisiert" (`[~] [/] [x] [^]`), begründet damit, dass §3 das
Wort schon führt und keine dritte Schwelle dazukommt. Der Nutzer hat
widersprochen: *„Eigentlich sollen fertige oder gar deployed Knoten gar nicht
auf den Lean-Path, da gibt es ja nichts mehr zu tun."* Das ist richtig, und
mein Argument gegen die engere Fassung fällt weg, sobald man sie aus den
**vorhandenen** Begriffen zusammensetzt: *realisiert, aber nicht erledigt*.
`isStarted = isRealized && !isDone` — kein neues Vokabular.
**Was dabei zu klären war: „auf dem Pfad" ist nicht „Station".** Seit D46
bekommt ein erledigter Knoten auf dem Pfad **keinen** Stationspunkt, **keine**
Pfadlinie und geht mit **0** in die Kosten; übrig bleibt allein, dass er nicht
zurücktritt — und das tut er per D46-Nachtrag ohnehin nie. Für den fertigen
Knoten **selbst** war der Unterschied zwischen beiden Schwellen also exakt
null. Er lag einzig darin, ob der Pfad **in eine fertige Zugabe hineinschaut**
und dort liegen gebliebene offene Kinder als Station zeigt. Auch das entfällt
jetzt, und zwar mit einem eigenen Argument aus §3: Wer unter einem `+`-Knoten
hängt, ist mit ihm zusammen entbehrlich — der Autor hat `[x]` geschrieben, ein
offener Rest darunter ist Buchhaltung, keine offene Front.
**Ehrlich zu benennen ist der Einwand gegen die ganze Regel:** Streng „am
günstigsten" wäre es, die angefangene Zugabe **abzubrechen** — Restkosten
gespart. Der Pfad zeigt seit D46 aber die offene Front, nicht das theoretische
Optimum. Und der Rückweg steht in der Notation schon: Wer die Zugabe wirklich
fallen lässt, schreibt `[-]`, und Verworfenes zählt nie.
**Dabei gefunden: dieselbe Lücke bei Alternativen — und die SPEC hatte recht,
der Code nicht.** §9 sagt seit D46 wörtlich: „Eine bereits realisierte
Alternative gewinnt, auch wenn eine unangetastete nominell billiger wäre — die
Wahl ist getroffen und bezahlt." Umgesetzt war das aber nur **über die
Kosten**, und die sind allein bei `[x]`/`[^]` null. Nachgemessen an
`= [~] A (L)` / `= [ ] B (S)`: Der Pfad wählte **B** und blasste das
angefangene A aus — das Bild widersprach damit der XOR-Regel des Plans, die
gerade sagt, dass A die realisierte Alternative ist. Also kein neues Feature,
sondern die fehlende Hälfte der Umsetzung: `chosenPool(kids)` schränkt die
Wahlmenge auf die realisierten Alternativen ein, sobald es welche gibt; die
Kostenregel (kleinste rekursive Kosten, Gleichstand ⇒ erste) gilt darin
unverändert.
**Mehrere realisierte Alternativen** sind in einer `=`-Gruppe schon per
`xorConflict` gemeldet, in einer `|`-Gruppe („mindestens eine") aber zulässig.
Dort entscheiden unter ihnen wieder die Kosten. Erwogen und **aufgeschoben**:
alle realisierten gemeinsam auf den Pfad zu nehmen. Das wäre ein größerer
Eingriff — eine any-of-Gruppe trüge dann nicht mehr genau eine Alternative —,
und die gewählte Lesart ist für sich verteidigbar: `|` erlaubt das Fallenlassen,
also ist „die billigere der begonnenen fertigstellen" ein gültiger günstigster
Weg.
**Nebengewinn bei der Suche (D42):** Eine Gruppe mit genau einer realisierten
Alternative ist entschieden und damit **keine freie Variable** mehr — sie
koppelt nicht und geht nicht in den Odometer ein. Die erschöpfende Suche wird
dadurch kleiner, nie größer.
**Nachgemessen.** Der gemeldete Baum: `#fe.rel` ist jetzt die Station, `#fe`
liegt auf dem Pfad ohne Punkt, `#fe.more` bleibt blass. Eine fertige Zugabe
mit offenem Kind bleibt samt Kind draußen. `= [~] A (L)` schlägt
`= [ ] B (S)`. Der **mitgelieferte Plan ändert sich nicht** (131 Pfadknoten,
27 Stationen, vorher wie nachher) — er hat keine angefangene Zugabe, und seine
einzige Gruppe mit realisierten Alternativen trägt zwei `[^]`, die schon
vorher beide 0 kosteten.
346 Tests, davon 10 neue. Gegenprobe: Nimmt man die Zugaben-Ausnahme wieder
heraus, fallen genau die fünf danach benannten Zusicherungen; nimmt man
`chosenPool` heraus, genau die zwei zu den Alternativen. Die Tests, die das
**unveränderte** Verhalten festhalten (unangetastete und erledigte Zugaben
bleiben draußen), bleiben in beiden Fällen grün.
+15 -4
View File
@@ -545,10 +545,21 @@ werden die für die günstigste Realisierung **nötigen** Knoten:
- **any of:** nur die **günstigste** Alternative ist nötig. „Günstig" =
kleinste rekursive Kosten (eigene T-Shirt-Größe plus — je Gate — Summe bzw.
Minimum der Kinder). Bei Gleichstand gewinnt die **erste** Alternative.
- **Optionale Knoten (`+`, §3) sind nie nötig** — sie zählen weder zu den
Kosten ihres Elternknotens noch liegen sie auf dem Pfad, und der Teilbaum
unter ihnen ebenso wenig. Genau dafür gibt es das Zeichen: Ohne `+` rechnet
der günstigste Pfad jede Zugabe ins Minimum ein und überschätzt es.
**Ist in der Gruppe etwas realisiert** (§3: `[~]`, `[/]`, `[x]`, `[^]`), ist
die Wahl damit getroffen — gewählt wird nur noch unter den realisierten
Alternativen, auch wenn eine unangetastete nominell billiger wäre. Sind es
mehrere (in einer `=`-Gruppe schon per `xorConflict` gemeldet, in einer
`|`-Gruppe zulässig), entscheidet unter ihnen wieder die Kostenregel.
- **Optionale Knoten (`+`, §3) sind nur nötig, solange an ihnen gearbeitet
wird** — also wenn sie realisiert (§3), aber noch nicht erledigt sind:
`[~]` und `[/]`. Sonst zählen sie weder zu den Kosten ihres Elternknotens
noch liegen sie auf dem Pfad, und der Teilbaum unter ihnen ebenso wenig.
Genau dafür gibt es das Zeichen: Ohne `+` rechnet der günstigste Pfad jede
Zugabe ins Minimum ein und überschätzt es. Die Ausnahme hält den umgekehrten
Fehler heraus — eine angefangene Zugabe ist offene Arbeit, und der Pfad
zeigt die offene Front. **Erledigte Zugaben bleiben draußen:** Dort ist
nichts mehr zu tun, und was darunter offen blieb, ist mit ihnen zusammen
entbehrlich (§3). Siehe D61.
- Verworfene Knoten zählen nie mit (unabhängig vom „verworfene einblenden"-
Filter).
- **Fehlende Größe wird als `M` gewertet** (nur für diese Kostenschätzung; die
+6
View File
@@ -43,6 +43,7 @@
- [^] #ed.path: Cheapest path (M)
- [^] #ed.path.cost: Cost model, missing size counts as M (S)
- [^] #ed.path.front: Only the open front: what is done costs nothing (S) %% see D46
- [x] #ed.path.started: Started work is on the path, extras included (S) %% see D61
- [^] #ed.path.line: Metro-map line through the leaves (S)
- [^] #ed.path.step: Step from station to station, one button (S) %% see D47
- [^] #ed.closure: Count the whole dependency closure (M)
@@ -385,6 +386,11 @@
what the plan would have cost from scratch. An alternative already built
therefore wins its group even against a cheaper untouched one.
#ed.path.started
An extra (`+`) joins the path while it is being worked on, and a started
alternative wins its group. Optionality says the plan can do without it;
the status says work has begun, and the front is made of exactly that.
#ed.path.line
A dashed curve threads through the open leaves of the path, with a pale
station dot at each. Borrowed from metro maps: the dot is large and faint so
+7 -5
View File
@@ -63,7 +63,8 @@ One node per line. Everything except the label is optional.
`[~]`, `[/]`, `[x]` or `[^]`); each additional realized one warns
(`xorConflict`).
- Optionality (`+`) is orthogonal to status: an optional node can be long
`[^]`. Optional nodes never count toward the cheapest path.
`[^]`. An optional node counts toward the cheapest path **only while it is
being worked on** — `[~]` or `[/]`; untouched and finished ones stay out.
### Fold mark (immediately before the label, i.e. after the status box)
@@ -96,10 +97,11 @@ One node per line. Everything except the label is optional.
- **`[x]` and `[^]` cost nothing any more.** The cheapest path prices the
work that is *left*, so a done node adds zero regardless of its size and
carries no station on the path line; started work (`[~]`, `[/]`) still
counts in full. A realized alternative therefore wins its `|`/`=` group
even when a cheaper unstarted one sits next to it. The *intrinsic* status
decides — dependencies may hold a node back effectively, but the work on
it is paid for.
counts in full. **A realized alternative wins its `|`/`=` group** even when
a cheaper unstarted one sits next to it — the choice has been made; among
several realized ones cost decides again. The *intrinsic* status decides —
dependencies may hold a node back effectively, but the work on it is paid
for.
### Size (effort)
+36 -12
View File
@@ -40,14 +40,18 @@ export function visibleChildren(n, showDiscarded){
any-of und XOR nur die günstigste Alternative. Günstig" = kleinste rekursive
Kosten (eigene Größe + Kinder; any-of das Minimum). Verworfene zählen nie
mit (unabhängig vom Einblenden-Toggle). Gleichstand erste. Fehlende
Größe = M.
Größe = M. Ist in einer disjunktiven Gruppe etwas realisiert, wird nur noch
unter den realisierten gewählt (`chosenPool`, D61).
Optionale Kinder (`+`, SPEC §3/D29) fallen hier ebenfalls heraus sie sind
per Definition entbehrlich, also weder Kostenanteil noch Pfadknoten. Da beide
Nutzer (`cheapestCost`, `markCheapest`) über diese Funktion gehen, gilt das
samt Teilbaum. */
per Definition entbehrlich, also weder Kostenanteil noch Pfadknoten. Da alle
Nutzer über diese Funktion gehen, gilt das samt Teilbaum.
AUSNAHME (D61): Wird an der Zugabe gerade gearbeitet (`isStarted`), liegt
sie auf dem Pfad angefangene Arbeit IST die offene Front. Erledigte
Zugaben bleiben draußen: Dort ist nichts mehr zu tun, und was darunter
offen blieb, ist mit ihnen zusammen entbehrlich (§3). */
export function pathChildren(n){
return n.children.filter(k =>
!k.optional && (!k.status || k.status.key !== 'verworfen'));
(!k.optional || isStarted(k)) && (!k.status || k.status.key !== 'verworfen'));
}
/* ---------- Erledigt: was nichts mehr kostet (SPEC §9, D46) ----------
`[x]` fertig und `[^]` in Produktion. Die Beförderung auf Prod ist keine
@@ -60,13 +64,29 @@ export function pathChildren(n){
export function isDone(n){
return !!n.status && (n.status.key === 'fertig' || n.status.key === 'prod');
}
/* realisiert" (SPEC §3, D35): Kosten investiert oder mehr. Trägt die
XOR-Regel und seit D61 die Wahl in Alternativgruppen. */
export function isRealized(n){
return !!n.status && ['arbeit', 'durchstich', 'fertig', 'prod'].includes(n.status.key);
}
/* Angefangen und noch offen: realisiert, aber nicht erledigt (D61). Aus zwei
vorhandenen Begriffen zusammengesetzt statt einer dritten Schwelle. */
export function isStarted(n){ return isRealized(n) && !isDone(n); }
/* Wahlmenge einer disjunktiven Gruppe (SPEC §9, D61): Ist etwas realisiert,
ist die Wahl getroffen dann wird nur noch unter den realisierten gewählt.
Sonst stehen alle zur Wahl. Die Kostenregel (kleinste rekursive Kosten,
Gleichstand erste) gilt innerhalb der Menge unverändert. */
export function chosenPool(kids){
const real = kids.filter(isRealized);
return real.length ? real : kids;
}
/* fehlende Größe wird als M interpretiert; Erledigtes kostet nichts mehr */
export function ownCost(n){ return isDone(n) ? 0 : SIZE_RANK[n.size || 'M'] + 1; }
export function cheapestCost(n){
const kids = pathChildren(n);
let c = ownCost(n);
if(kids.length){
if(gateOf(kids) !== 'and') c += Math.min(...kids.map(cheapestCost));
if(gateOf(kids) !== 'and') c += Math.min(...chosenPool(kids).map(cheapestCost));
else c += kids.reduce((s, k) => s + cheapestCost(k), 0);
}
return c;
@@ -129,10 +149,14 @@ export function computeCheapPlan(roots){
}
})(roots);
for(const kids of groups){
let best = kids[0], bc = cheapestCost(kids[0]);
for(const k of kids.slice(1)){ const c = cheapestCost(k); if(c < bc){ bc = c; best = k; } }
const pool = chosenPool(kids);
let best = pool[0], bc = cheapestCost(pool[0]);
for(const k of pool.slice(1)){ const c = cheapestCost(k); if(c < bc){ bc = c; best = k; } }
localChoice.set(kids[0], best); /* Schlüssel: erstes Kind der Gruppe */
if(anyDeps && kids.some(k => touches.get(k))) coupled.push(kids);
/* Eine entschiedene Gruppe (genau eine realisierte Alternative, D61) ist
keine freie Variable mehr sie koppelt nicht und verkleinert die Suche. */
if(anyDeps && pool.length > 1 && pool.some(k => touches.get(k)))
coupled.push({key: kids[0], pool});
}
/* Nötige Menge für eine Belegung der gekoppelten Gruppen. */
@@ -159,7 +183,7 @@ export function computeCheapPlan(roots){
const costOf = set => { let c = 0; set.forEach(n => c += ownCost(n)); return c; };
let product = 1;
for(const kids of coupled){ product *= kids.length; if(product > EXACT_LIMIT) break; }
for(const grp of coupled){ product *= grp.pool.length; if(product > EXACT_LIMIT) break; }
if(product > EXACT_LIMIT){
/* Gierig, aber benannt (D42): lokale Wahl überall, Hülle trotzdem. */
return {set: needed(new Map()), exact: false};
@@ -171,12 +195,12 @@ export function computeCheapPlan(roots){
let best = null, bc = Infinity;
for(;;){
const choice = new Map();
coupled.forEach((kids, g) => choice.set(kids[0], kids[idx[g]]));
coupled.forEach((grp, i) => choice.set(grp.key, grp.pool[idx[i]]));
const set = needed(choice);
const c = costOf(set);
if(c < bc){ bc = c; best = set; }
let g = coupled.length - 1;
while(g >= 0 && ++idx[g] >= coupled[g].length){ idx[g] = 0; g--; }
while(g >= 0 && ++idx[g] >= coupled[g].pool.length){ idx[g] = 0; g--; }
if(g < 0) break;
}
return {set: best, exact: true};
+14 -2
View File
@@ -80,10 +80,22 @@ describe('Auswahl — die getroffene Wahl gewinnt', () => {
| [ ] Billig (XS)`)).toEqual(['Gemacht', 'Wahl']);
});
it('eine erst angefangene Alternative gewinnt dadurch NICHT', () => {
/* Bis D61 gewann hier Billig": Die Wahl lief allein über die Kosten, und
die sind nur bei `[x]`/`[^]` null. Angefangenes kostet voll und verlor
damit obwohl SPEC §9 die Regel schon so formuliert hatte. */
it('gilt auch für eine erst angefangene Alternative', () => {
expect(cheapLabels(`[ ] Wahl (XS)
| [~] Angefangen (L)
| [ ] Billig (XS)`)).toEqual(['Billig', 'Wahl']);
| [ ] Billig (XS)`)).toEqual(['Angefangen', 'Wahl']);
});
/* Die unangetastete ist hier die billigste von allen gewählt wird
trotzdem unter den realisierten, und dort entscheiden die Kosten. */
it('lässt unter mehreren realisierten wieder die Kosten entscheiden', () => {
expect(cheapLabels(`[ ] Wahl (XS)
| [~] Teuer (L)
| [/] Günstig (S)
| [ ] Unangetastet (XS)`)).toEqual(['Günstig', 'Wahl']);
});
it('gilt auch in einer XOR-Gruppe', () => {
+55 -2
View File
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { parse } from '../src/parser.js';
import { computeCheapSet, cheapestCost, visibleChildren } from '../src/model.js';
import { computeCheapSet, cheapestCost, cheapCls, visibleChildren } from '../src/model.js';
import { renderTreeHtml } from '../src/render.js';
const t = key => key;
@@ -38,7 +38,7 @@ describe('Parser — `+` setzt optional, nicht das Gate', () => {
});
});
describe('Günstigster Pfad — optionale Knoten sind nie nötig', () => {
describe('Günstigster Pfad — unangetastete Zugaben sind nicht nötig', () => {
const BAUM = `[ ] Wurzel (S)
- [ ] Pflicht (S)
+ [ ] Zugabe (XXL)
@@ -69,6 +69,59 @@ describe('Günstigster Pfad — optionale Knoten sind nie nötig', () => {
});
});
/* Die Ausnahme (D61): Wird an der Zugabe gearbeitet, ist sie offene Arbeit
und der Pfad zeigt seit D46 die offene Front. Erledigte bleiben draußen. */
describe('Günstigster Pfad — an einer angefangenen Zugabe wird gearbeitet', () => {
it('nimmt eine `[~]`-Zugabe auf den Pfad', () => {
expect(cheapLabels(`[ ] Wurzel (S)\n - [ ] Pflicht (S)\n + [~] Zugabe (S)`))
.toEqual(['Pflicht', 'Wurzel', 'Zugabe']);
});
it('nimmt eine `[/]`-Zugabe ebenso', () => {
expect(cheapLabels(`[ ] Wurzel (S)\n + [/] Zugabe (S)`))
.toEqual(['Wurzel', 'Zugabe']);
});
it('lässt `[?]`, `[ ]`, `[!]` und ohne Status weiterhin draußen', () => {
expect(cheapLabels(`[ ] Wurzel (S)
+ [?] Idee (S)
+ [ ] Geplant (S)
+ [!] Riskant (S)
+ Neutral (S)`)).toEqual(['Wurzel']);
});
/* Genau der Punkt, an dem die Empfehlung korrigiert wurde: Fertiges gehört
nicht auf die offene Front und was darunter offen blieb, ist mit der
Zugabe zusammen entbehrlich (§3). */
it('lässt eine erledigte Zugabe samt offenem Rest draußen', () => {
expect(cheapLabels(`[ ] Wurzel (S)
+ [x] Fertige Zugabe (S)
- [ ] Rest (M)
+ [^] Live (S)`)).toEqual(['Wurzel']);
});
it('nimmt den Teilbaum der angefangenen Zugabe mit', () => {
expect(cheapLabels(`[ ] Wurzel (S)\n + [~] Zugabe (S)\n - [ ] Teil (S)`))
.toEqual(['Teil', 'Wurzel', 'Zugabe']);
});
it('rechnet sie damit auch in die Kosten des Elternknotens', () => {
/* Wurzel (S=2) + Zugabe (S=2) = 4; unangetastet wären es 2. */
expect(cheapestCost(roots(`[ ] Wurzel (S)\n + [~] Zugabe (S)`)[0])).toBe(4);
});
it('macht sie zur Station statt des Elternknotens', () => {
const [wurzel] = roots(`[ ] Wurzel (S)\n + [~] Zugabe (S)`);
const set = computeCheapSet([wurzel]);
expect(cheapCls(wurzel, set, false)).toBe('cheap');
expect(cheapCls(wurzel.children[0], set, false)).toBe('cheap cheap-leaf');
});
it('lässt eine verworfene Zugabe auch angefangen draußen', () => {
expect(cheapLabels(`[ ] Wurzel (S)\n + [-] Abgebrochen (S)`)).toEqual(['Wurzel']);
});
});
describe('Renderer — Kennzeichnung und Gemischt-Warnung', () => {
it('gibt dem optionalen Knoten die Klasse `opt`', () => {
const {html} = render(`[ ] Wurzel\n - [ ] Pflicht\n + [ ] Zugabe`);