feat: Die Größe bepreist den ganzen Teilbaum — Kinder zählen nicht obendrauf (D69)

Der Preis eines Knotens ist seine angegebene (oder D66-geschätzte) Größe;
die frühere Rekursion (eigene Größe plus Summe/Minimum der Kinder) zählte
doppelt und bestrafte sorgfältig zerlegte Pakete — eine zerlegte
S-Alternative verlor gegen ein grobes L. Das Vereinigungs-Maß der
Closure-Suche (D42) zählt je Knoten nur noch die Marge über seine nötigen
Teilpakete hinaus. Mitgelieferter Plan wählt identisch (per Vergleich
gemessen); SPEC §9 und llms.md nachgezogen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-25 10:45:12 +02:00
co-authored by Claude Fable 5
parent 7278e74393
commit 4aaa19becd
9 changed files with 227 additions and 33 deletions
+1
View File
@@ -19,6 +19,7 @@ reverse.
## 2026-08-25 ## 2026-08-25
- A size prices its whole subtree on the cheapest path — sub-packages no longer add on top, so a carefully decomposed `(S)` beats a coarse `(L)`
- The size is now the last free-standing `(L)`-style token of the line — earlier ones stay in the title, and `"(L)"` or `((L))` mention a size literally - The size is now the last free-standing `(L)`-style token of the line — earlier ones stay in the title, and `"(L)"` or `((L))` mention a size literally
- Fix: Ctrl+click on a dependency preceded by a non-breaking space did nothing, although the parser reads it as a dependency - Fix: Ctrl+click on a dependency preceded by a non-breaking space did nothing, although the parser reads it as a dependency
- Ctrl+click a dependency `:#id` in the text jumps to the line that defines the ID — Ctrl+Enter does the same at the caret - Ctrl+click a dependency `:#id` in the text jumps to the line that defines the ID — Ctrl+Enter does the same at the caret
+62
View File
@@ -5098,3 +5098,65 @@ selben Zug (Schritt-3-Regel, Größen-Abschnitt, Zitier-Faustregel).
per Mutation: erster statt letzter Treffer → genau der Letztes-gewinnt-Test per Mutation: erster statt letzter Treffer → genau der Letztes-gewinnt-Test
fällt; Anker entfernt → genau die zwei Alleinstehend-/Zitier-Tests. Alle fällt; Anker entfernt → genau die zwei Alleinstehend-/Zitier-Tests. Alle
Snapshots (kanonisches Beispiel §10) unverändert. Snapshots (kanonisches Beispiel §10) unverändert.
## D69 — Die Größe bepreist den ganzen Teilbaum: Kinder kommen nicht obendrauf
Gemeldet am eigenen Beispiel: Eine `|`-Gruppe mit `Manuell mit Downtime (S)`
(zerlegt in XS + S + eine Zugabe) verlor gegen `Failover (L)` — der Pfad
rechnete S(2) + XS(1) + S(2) = 5 gegen L(4). Der Einwand des Nutzers trifft
das Modell im Kern: **„Manuell mit Downtime" ist mit (S) bepreist — dann ist
egal, wie teuer die Summe der Teilpakete ist.** Ob die Zerlegung in die
Größe passt, prüft seit D62 der Größen-Konflikt; die Pfadrechnung hat die
Bewertung nicht anzuzweifeln. Das alte Modell (D18: „eigene Größe plus
Summe/Minimum der Kinder") zählte doppelt und bestrafte damit systematisch
genau die Pläne, die sorgfältig zerlegen — je ehrlicher die Zerlegung, desto
teurer sah das Paket aus.
**Neue Regel: Der Preis eines Knotens ist seine Größe.** Angegeben oder —
seit D66 — aus den Teilpaketen geschätzt; die Schätzung ist dieselbe Sorte
Gesamtaussage und übernimmt die Rolle nahtlos. Erledigtes bleibt 0 (D46).
`cheapestCost()` kollabiert damit auf `ownCost()`: Die Rekursion über die
Kinder entfällt, die Aggregation für größenlose Knoten trägt allein die
D66-Schätzregel. Die Wahl in einer Alternativgruppe vergleicht schlicht die
Größen der Alternativen.
**Die Bewertung gilt auch, wenn die Kinder sie sprengen.** Vier `(S)` unter
einem `(S)` sind ein `sizeConflict` — der Marker zeigt es, aber der Knoten
bleibt bewertet, wie er bewertet wurde (ausdrückliche Nutzer-Entscheidung:
„der übergeordnete Knoten bleibt dennoch S"). Die Alternative — die Kinder
könnten den Preis anheben (max-Variante) — war vorgeschlagen und ist damit
verworfen: Sie hätte die Bewertung des Autors stillschweigend überstimmt,
und für den Widerspruch gibt es bereits den lauten Kanal.
**Das Vereinigungs-Maß der Closure-Suche (D42) zieht mit.** Die Suche über
die gekoppelten Gruppen verglich Belegungen über die Summe der Knoten-Preise
der nötigen Menge — mit Teilbaum-Bepreisung zählte ein zerlegtes
Abhängigkeits-Ziel dann mehrfach (Eltern plus Kinder), ein grobes nur
einmal: dieselbe Zerlegungs-Strafe, nur im Suchmaß. Jeder nötige Knoten
zählt jetzt nur mit dem, was seine Größe **über die nötigen Teilpakete
hinaus** behauptet (nie negativ); die Summe dieser Margen bepreist einen
vollständig zerlegten Teilbaum mit seiner Spitzengröße, gemeinsam
Gebrauchtes zählt über die Mengen-Vereinigung weiterhin einmal. Benannte
Grenze: Wo Kinder ihre Elterngröße im Rangraum übersteigen, liegt das
Suchmaß über dem Preis der lokalen Wahl — die beiden Maße sind dann nicht
identisch; das betrifft genau die Teilbäume, die der Größen-Konflikt ohnehin
anmahnt oder die eng bepreist sind, und die lokale Wahl (der Regelfall)
folgt strikt der Nutzer-Regel.
**Benannter Verlust:** Erledigte Teilpakete unter einem offenen Knoten
senken dessen Preis nicht mehr (kein Kinder-Summieren, keine Bruchteile —
D46 lehnte anteilige Restkosten schon ab). Für die Wahl trägt das kaum:
Sobald an einer Alternative selbst gearbeitet ist, entscheidet ohnehin
`chosenPool` (D61); Stationen und Markierung behalten ihre Knoten-Genauigkeit
unverändert.
**Nachgemessen:** Der gemeldete Fall wählt jetzt „Manuell mit Downtime"
(S=2 gegen L=4). Der mitgelieferte Werkbaum-Plan wählt unter altem und neuem
Modell **identisch** (137 Pfadknoten, 57 Stationen, gleiche Menge — per
git-stash-Vergleich gemessen, nicht angenommen). 450 Tests, davon 5 neue in
`tests/pricing.test.js`; drei Alt-Tests, die die Summen-Semantik festhielten,
sind auf die neue Regel umgeschrieben. Gegenproben per Mutation: alte
Rekursion zurückgebaut → genau die drei danach benannten Zusicherungen
fallen; Vereinigungs-Maß auf schlichte Summe zurück → genau der eine
Marginal-Test (dessen erste Fassung nicht unterschied, weil Wurzeln immer
nötig sind — der Testbaum musste die Ziele unter einen unangetasteten
`+`-Zweig legen).
+26 -12
View File
@@ -598,18 +598,28 @@ wird die **noch offene** Arbeit — der Pfad beantwortet „was ist als Nächste
am günstigsten?", nicht „was hätte der Plan von vorn gekostet?". Ermittelt am günstigsten?", nicht „was hätte der Plan von vorn gekostet?". Ermittelt
werden die für die günstigste Realisierung **nötigen** Knoten: werden die für die günstigste Realisierung **nötigen** Knoten:
- **Die Größe bepreist den ganzen Teilbaum.** Der Preis eines Knotens ist
seine **angegebene Größe** (§5) — die Teilpakete kommen **nicht** noch
einmal obendrauf: Wer ein Paket mit `(S)` bewertet hat, hat den Teilbaum
bewertet. Ob die Zerlegung in die Größe passt, prüft der Größen-Konflikt
(§5) — die Pfadrechnung zweifelt die Bewertung nicht an. Fehlt die Größe,
vertritt die Schätzung aus den Teilpaketen sie (unten). Siehe D69 (bis
dahin galt: eigene Größe **plus** Summe/Minimum der Kinder — das
bestrafte gerade die sorgfältig zerlegten Pakete).
- **Erledigtes kostet nichts mehr:** Ein Knoten mit `[x]` oder `[^]` (§4) geht - **Erledigtes kostet nichts mehr:** Ein Knoten mit `[x]` oder `[^]` (§4) geht
mit **0** in die Rechnung, unabhängig von seiner Größe. Angefangenes (`[~]`, mit **0** in die Rechnung, unabhängig von seiner Größe. Angefangenes (`[~]`,
`[/]`) zählt dagegen **voll** — die Arbeit ist noch offen. Maßgeblich ist der `[/]`) zählt dagegen **voll** — die Arbeit ist noch offen. Maßgeblich ist der
**intrinsische** Status: Investiert ist investiert, auch wenn Abhängigkeiten **intrinsische** Status: Investiert ist investiert, auch wenn Abhängigkeiten
den Knoten effektiv zurückhalten (§4). Abgezogen werden nur die **eigenen** den Knoten effektiv zurückhalten (§4). Offene Teilpakete unter einem
Kosten des Knotens, nicht die seines Teilbaums. Folge in Alternativgruppen: erledigten Knoten bleiben auf dem Pfad und behalten ihre Stationen — die
Eine bereits realisierte Alternative gewinnt, auch wenn eine unangetastete Kostenfrage stellt sich dort nicht mehr, die Wahl ist realisiert. Folge in
nominell billiger wäre — die Wahl ist getroffen und bezahlt. Siehe D46. Alternativgruppen: Eine bereits realisierte Alternative gewinnt, auch wenn
eine unangetastete nominell billiger wäre — die Wahl ist getroffen und
bezahlt. Siehe D46.
- **all of:** alle Kinder sind nötig. - **all of:** alle Kinder sind nötig.
- **any of:** nur die **günstigste** Alternative ist nötig. „Günstig" = - **any of:** nur die **günstigste** Alternative ist nötig. „Günstig" ist die
kleinste rekursive Kosten (eigene T-Shirt-Größe plus — je Gate — Summe bzw. **Größe der Alternative selbst** — angegeben oder geschätzt (oben/unten).
Minimum der Kinder). Bei Gleichstand gewinnt die **erste** Alternative. Bei Gleichstand gewinnt die **erste** Alternative.
**Ist in der Gruppe etwas realisiert** (§3: `[~]`, `[/]`, `[x]`, `[^]`), ist **Ist in der Gruppe etwas realisiert** (§3: `[~]`, `[/]`, `[x]`, `[^]`), ist
die Wahl damit getroffen — gewählt wird nur noch unter den realisierten die Wahl damit getroffen — gewählt wird nur noch unter den realisierten
Alternativen, auch wenn eine unangetastete nominell billiger wäre. Sind es Alternativen, auch wenn eine unangetastete nominell billiger wäre. Sind es
@@ -617,10 +627,10 @@ werden die für die günstigste Realisierung **nötigen** Knoten:
`|`-Gruppe zulässig), entscheidet unter ihnen wieder die Kostenregel. `|`-Gruppe zulässig), entscheidet unter ihnen wieder die Kostenregel.
- **Optionale Knoten (`+`, §3) sind nur nötig, solange an ihnen gearbeitet - **Optionale Knoten (`+`, §3) sind nur nötig, solange an ihnen gearbeitet
wird** — also wenn sie realisiert (§3), aber noch nicht erledigt sind: wird** — also wenn sie realisiert (§3), aber noch nicht erledigt sind:
`[~]` und `[/]`. Sonst zählen sie weder zu den Kosten ihres Elternknotens `[~]` und `[/]`. Sonst liegen sie nicht auf dem Pfad (der Teilbaum unter
noch liegen sie auf dem Pfad, und der Teilbaum unter ihnen ebenso wenig. ihnen ebenso wenig) und zählen nicht zur Größen-Schätzung (unten).
Genau dafür gibt es das Zeichen: Ohne `+` rechnet der günstigste Pfad jede Genau dafür gibt es das Zeichen: Ohne `+` läge jede
Zugabe ins Minimum ein und überschätzt es. Die Ausnahme hält den umgekehrten Zugabe auf dem günstigsten Pfad. Die Ausnahme hält den umgekehrten
Fehler heraus — eine angefangene Zugabe ist offene Arbeit, und der Pfad Fehler heraus — eine angefangene Zugabe ist offene Arbeit, und der Pfad
zeigt die offene Front. **Erledigte Zugaben bleiben draußen:** Dort ist zeigt die offene Front. **Erledigte Zugaben bleiben draußen:** Dort ist
nichts mehr zu tun, und was darunter offen blieb, ist mit ihnen zusammen nichts mehr zu tun, und was darunter offen blieb, ist mit ihnen zusammen
@@ -654,7 +664,11 @@ Gebrauchtes zählt über die Mengen-Vereinigung nur **einmal**. Regeln:
bezahlt werden. **Verfahren (D42): erschöpfende Suche** über die bezahlt werden. **Verfahren (D42): erschöpfende Suche** über die
**gekoppelten** Gruppen (Gruppen, deren Teilbäume Abhängigkeiten enthalten **gekoppelten** Gruppen (Gruppen, deren Teilbäume Abhängigkeiten enthalten
oder gebraucht werden); alle übrigen wählen lokal wie bisher, bei oder gebraucht werden); alle übrigen wählen lokal wie bisher, bei
Gleichstand die erste. Wird die Suche zu groß, rechnet die Anzeige **gierig Gleichstand die erste. Verglichen werden die Belegungen über die Summe
dessen, was jeder nötige Knoten **über seine nötigen Teilpakete hinaus**
behauptet (nie negativ) — so bepreist auch die Vereinigung einen Teilbaum
mit seiner Größe, statt Zerlegungstiefe zu bestrafen (D69). Wird die Suche
zu groß, rechnet die Anzeige **gierig
und sagt es** — Warnung `cheapApprox`, zeilenlos. und sagt es** — Warnung `cheapApprox`, zeilenlos.
Darstellung per **Inversion**: nicht benötigte Knoten (nicht-gewählte Darstellung per **Inversion**: nicht benötigte Knoten (nicht-gewählte
+4 -3
View File
@@ -43,7 +43,7 @@
- [^] #ed.deplink.calm: Visually secondary — the tree carries the message (S) - [^] #ed.deplink.calm: Visually secondary — the tree carries the message (S)
- [^] #ed.deplink.focus: Highlight what a selected node needs and feeds (S) - [^] #ed.deplink.focus: Highlight what a selected node needs and feeds (S)
- [^] #ed.path: Cheapest path (XL) - [^] #ed.path: Cheapest path (XL)
- [^] #ed.path.cost: Cost model, sizes summed and minimized (S) - [x] #ed.path.cost: Cost model, a size prices its subtree (S)
- [^] #ed.path.est: Missing size estimated from the sub-packages (S) %% see D66 - [^] #ed.path.est: Missing size estimated from the sub-packages (S) %% see D66
- [^] #ed.path.front: Only the open front: what is done costs nothing (S) %% see D46 - [^] #ed.path.front: Only the open front: what is done costs nothing (S) %% see D46
- [^] #ed.path.started: Started work is on the path, extras included (S) %% see D61 - [^] #ed.path.started: Started work is on the path, extras included (S) %% see D61
@@ -394,8 +394,9 @@
the cheapest alternative. This is the feature the whole notation exists for. the cheapest alternative. This is the feature the whole notation exists for.
#ed.path.cost #ed.path.cost
Cost is the T-shirt size, summed along and-groups and minimized along The cost of a node is its stated (or estimated) T-shirt size — the size
or-groups. prices the whole subtree, sub-packages are not added on top. Whether they
fit is the size conflict check's job.
#ed.path.est #ed.path.est
A node without a size is estimated from its sub-packages: at least the A node without a size is estimated from its sub-packages: at least the
+4
View File
@@ -111,6 +111,10 @@ One node per line. Everything except the label is optional.
label. `"(L)"` and `((L))` mention a size literally (quoting convention). label. `"(L)"` and `((L))` mention a size literally (quoting convention).
- From `(M)` upward a node **should be decomposed further**; a node ≥ M - From `(M)` upward a node **should be decomposed further**; a node ≥ M
without children gets a placeholder hint in the diagram. without children gets a placeholder hint in the diagram.
- **A size prices its whole subtree.** For the cheapest path, the cost of a
node is its own stated size — sub-packages are **not** added on top; the
conflict check below guards whether they fit. A done node (`[x]`/`[^]`)
costs 0.
- For cost estimation a missing size is **estimated from the sub-packages**: - For cost estimation a missing size is **estimated from the sub-packages**:
at least the largest size among the counting children (same children as at least the largest size among the counting children (same children as
the conflict check below, except unsized children count too — estimated the conflict check below, except unsized children count too — estimated
+22 -10
View File
@@ -118,15 +118,14 @@ export function assumedSize(n){
} }
/* fehlende Größe wird geschätzt (assumedSize); Erledigtes kostet nichts mehr */ /* fehlende Größe wird geschätzt (assumedSize); Erledigtes kostet nichts mehr */
export function ownCost(n){ return isDone(n) ? 0 : SIZE_RANK[assumedSize(n)] + 1; } export function ownCost(n){ return isDone(n) ? 0 : SIZE_RANK[assumedSize(n)] + 1; }
export function cheapestCost(n){ /* Die Größe bepreist den GANZEN Teilbaum (SPEC §9, D69): Wer ein Paket mit
const kids = pathChildren(n); (S) bewertet hat, hat den Teilbaum bewertet — die Teilpakete kommen nicht
let c = ownCost(n); noch einmal obendrauf. Ob sie in die Größe passen, prüft der
if(kids.length){ Größen-Konflikt (§5/D62); fehlt die Größe, vertritt die Schätzung sie
if(gateOf(kids) !== 'and') c += Math.min(...chosenPool(kids).map(cheapestCost)); (assumedSize, D66). Der Preis einer Alternative ist damit schlicht ihr
else c += kids.reduce((s, k) => s + cheapestCost(k), 0); eigener — die frühere Rekursion (eigene Größe plus Summe/Minimum der
} Kinder) bestrafte gerade die sorgfältig zerlegten Pakete. */
return c; export function cheapestCost(n){ return ownCost(n); }
}
/* ---------- Günstigster Pfad auf der Dependency Closure (SPEC §9, D42) ---- /* ---------- Günstigster Pfad auf der Dependency Closure (SPEC §9, D42) ----
Mit Abhängigkeiten zählt nicht mehr der gewählte Teilbaum, sondern die Mit Abhängigkeiten zählt nicht mehr der gewählte Teilbaum, sondern die
HÜLLE: Jeder nötige Knoten zieht seine `:#…`-Ziele samt deren Realisierung HÜLLE: Jeder nötige Knoten zieht seine `:#…`-Ziele samt deren Realisierung
@@ -216,7 +215,20 @@ export function computeCheapPlan(roots){
} }
return set; return set;
}; };
const costOf = set => { let c = 0; set.forEach(n => c += ownCost(n)); return c; }; /* Preis einer Belegung (D69): Jeder nötige Knoten zählt nur mit dem, was
seine Größe über die nötigen Teilpakete HINAUS behauptet (nie negativ) —
die Summe bepreist einen Teilbaum so mit seiner Größe, statt
Zerlegungstiefe zu bestrafen; gemeinsam Gebrauchtes zählt über die
Mengen-Vereinigung weiterhin nur einmal. */
const costOf = set => {
let c = 0;
set.forEach(n => {
let kids = 0;
for(const k of pathChildren(n)) if(set.has(k)) kids += ownCost(k);
c += Math.max(0, ownCost(n) - kids);
});
return c;
};
let product = 1; let product = 1;
for(const grp of coupled){ product *= grp.pool.length; if(product > EXACT_LIMIT) break; } for(const grp of coupled){ product *= grp.pool.length; if(product > EXACT_LIMIT) break; }
+7 -2
View File
@@ -65,10 +65,15 @@ describe('Kosten — Erledigtes zählt nicht mehr', () => {
expect(ownCost(a)).toBe(0); expect(ownCost(a)).toBe(0);
}); });
it('zieht nur die eigenen Kosten ab, nicht den Teilbaum', () => { it('der erledigte Knoten bepreist 0; sein offenes Kind bleibt auf dem Pfad', () => {
/* Seit D69 bepreist die Größe den ganzen Teilbaum — der erledigte
Elternknoten kostet nichts mehr, das offene Kind bleibt trotzdem
nötig und behält seinen eigenen Preis. */
const [n] = roots(`[x] Eltern (L) const [n] = roots(`[x] Eltern (L)
- [ ] Kind (S)`); - [ ] Kind (S)`);
expect(cheapestCost(n)).toBe(cheapestCost(roots('[ ] Kind (S)')[0])); expect(cheapestCost(n)).toBe(0);
const set = computeCheapSet([n]);
expect(set.has(n.children[0])).toBe(true);
}); });
}); });
+11 -6
View File
@@ -48,9 +48,11 @@ describe('Günstigster Pfad — unangetastete Zugaben sind nicht nötig', () =>
expect(cheapLabels(BAUM)).toEqual(['Pflicht', 'Wurzel']); expect(cheapLabels(BAUM)).toEqual(['Pflicht', 'Wurzel']);
}); });
it('rechnet die Zugabe nicht in die Kosten des Elternknotens', () => { it('die bewertete Wurzel bepreist ihren Teilbaum — die Zugabe erst recht nicht', () => {
/* Wurzel (S=2) + Pflicht (S=2) = 4; mit der XXL-Zugabe wären es 10. */ /* Seit D69 ist der Preis die eigene Größe: Wurzel (S) = 2, unabhängig
expect(cheapestCost(roots(BAUM)[0])).toBe(4); von Pflicht und XXL-Zugabe. Dass Zugaben auch die GESCHÄTZTE Größe
eines größenlosen Knotens nicht erhöhen, sichern die D66-Tests. */
expect(cheapestCost(roots(BAUM)[0])).toBe(2);
}); });
it('vergleicht Alternativen ohne deren Zugaben', () => { it('vergleicht Alternativen ohne deren Zugaben', () => {
@@ -105,9 +107,12 @@ describe('Günstigster Pfad — an einer angefangenen Zugabe wird gearbeitet', (
.toEqual(['Teil', 'Wurzel', 'Zugabe']); .toEqual(['Teil', 'Wurzel', 'Zugabe']);
}); });
it('rechnet sie damit auch in die Kosten des Elternknotens', () => { it('die angefangene Zugabe behält ihren eigenen Preis auf dem Pfad', () => {
/* Wurzel (S=2) + Zugabe (S=2) = 4; unangetastet wären es 2. */ /* Seit D69 bepreist die Wurzel nur sich selbst (S = 2); die angefangene
expect(cheapestCost(roots(`[ ] Wurzel (S)\n + [~] Zugabe (S)`)[0])).toBe(4); Zugabe liegt mit ihrem eigenen Preis daneben auf dem Pfad. */
const [wurzel] = roots(`[ ] Wurzel (S)\n + [~] Zugabe (S)`);
expect(cheapestCost(wurzel)).toBe(2);
expect(cheapestCost(wurzel.children[0])).toBe(2);
}); });
it('macht sie zur Station statt des Elternknotens', () => { it('macht sie zur Station statt des Elternknotens', () => {
+90
View File
@@ -0,0 +1,90 @@
import { describe, it, expect } from 'vitest';
import { parse } from '../src/parser.js';
import { cheapestCost, computeCheapSet, computeCheapPlan } from '../src/model.js';
/* Die Größe bepreist den ganzen Teilbaum (SPEC §9, D69): Der Preis eines
Knotens ist seine angegebene (oder nach D66 geschätzte) Größe — die
Teilpakete kommen nicht noch einmal obendrauf. Ob sie hineinpassen, prüft
der Größen-Konflikt (§5/D62); die Pfadrechnung zweifelt die Bewertung
nicht an. */
const roots = txt => parse(txt).roots;
const cheapLabels = txt => {
const r = roots(txt);
return [...computeCheapSet(r)].map(n => n.label).sort();
};
describe('Die Größe bepreist den Teilbaum (D69)', () => {
it('der gemeldete Fall: die zerlegte S-Alternative schlägt das L', () => {
/* Vorher: Manuell = S(2) + XS(1) + S(2) = 5 > Failover L(4) — die
sorgfältige Zerlegung wurde bestraft. Jetzt zählt die Bewertung:
S(2) < L(4). */
const txt = `- kc
- Verfügbarkeit
| Manuell mit Downtime (S)
- Maintainance Mode (XS)
- Recovery Plan (S)
+ Seite Wartungsarbeiten (S)
| Failover (L)
| HA (XXL)`;
const labels = cheapLabels(txt);
expect(labels).toContain('Manuell mit Downtime');
expect(labels).not.toContain('Failover');
});
it('der Preis eines bewerteten Knotens ist seine Größe, Kinder egal', () => {
const [n] = roots(`- [ ] Paket (S)
- [ ] A (XS)
- [ ] B (S)`);
expect(cheapestCost(n)).toBe(2);
});
it('die Bewertung gilt auch, wenn die Kinder zu groß sind (D62 warnt dann)', () => {
/* Vier S unter einem S sind ein sizeConflict — der Fehlermarker meldet
es, aber der Knoten bleibt bewertet, wie er bewertet wurde. */
const txt = `- [ ] Wahl (XS)
| [ ] A (S)
- [ ] a1 (S)
- [ ] a2 (S)
- [ ] a3 (S)
- [ ] a4 (S)
| [ ] B (M)`;
expect(parse(txt).warnings.some(w => w.type === 'sizeConflict')).toBe(true);
const labels = cheapLabels(txt);
expect(labels).toContain('A');
expect(labels).not.toContain('B');
});
it('ohne Größe vertritt die D66-Schätzung die Bewertung', () => {
/* Alternative ohne Größe mit einem L-Kind wird als L geschätzt und
verliert gegen das bewertete M. */
const labels = cheapLabels(`- [ ] Wahl (XS)
| [ ] Unbewertet
- [ ] Kind (L)
| [ ] Bewertet (M)`);
expect(labels).toContain('Bewertet');
expect(labels).not.toContain('Unbewertet');
});
it('das Vereinigungs-Maß bestraft Zerlegungstiefe nicht (D42/D69)', () => {
/* Beide Alternativen ziehen je ein M-schweres Ziel: A ein zerlegtes
(M mit zwei S-Kindern), B ein grobes (L). Über die Marginal-Summe
wiegen beide 4 — Gleichstand, die erste Alternative (A) gewinnt.
Die frühere Knoten-Summe zählte das zerlegte Ziel mit 3+2+2 = 7 und
ließe B gewinnen. */
const txt = `- [ ] Wurzel (XS)
- [ ] Wahl (XS)
| [ ] A (S) :#t1
| [ ] B (S) :#t2
+ [ ] Extras (XS)
- [ ] #t1: Zerlegt (M)
- [ ] t1a (S)
- [ ] t1b (S)
- [ ] #t2: Grob (L)`;
const { set, exact } = computeCheapPlan(roots(txt));
const labels = [...set].map(n => n.label);
expect(exact).toBe(true);
expect(labels).toContain('A');
expect(labels).not.toContain('B');
});
});