feat: der günstigste Pfad zeigt die offene Front (D46)

Der Pfad rechnete rein aus T-Shirt-Größen — `ownCost()` war
`SIZE_RANK[size] + 1`, der Status kam in der Kostenrechnung überhaupt nicht
vor (nur `[-]` flog heraus). Folge: Längst Erledigtes wurde voll eingepreist
und lag weiter hell auf dem Pfad; im mitgelieferten Werkbaum-Plan zeichnete
die Linie damit überwiegend fertige Arbeit nach. Und in einer Alternativgruppe
hatte eine bereits realisierte Alternative keinerlei Kostenvorteil — der Pfad
empfahl die nominell billigere, obwohl die Wahl längst getroffen und bezahlt
ist.

Ab jetzt kostet Erledigtes 0. Der Pfad beantwortet damit „was ist als
Nächstes am günstigsten?" statt „was hätte der Plan von vorn gekostet?".

- Schwelle bei `[x]` fertig: die Beförderung auf `[^]` ist keine Kostenfrage
  (das tut per D30 ein eigener Commit).
- Angefangenes (`[~]`, `[/]`) zählt weiter voll — Bruchteile ordinaler Größen
  wären erfunden, und `[~]` heißt laut §4 gerade „Risiko hoch".
- Maßgeblich ist der intrinsische Status: investiert ist investiert, auch wenn
  Abhängigkeiten den Knoten effektiv zurückhalten (D39). Verhindert nebenbei
  doppeltes Zählen — die Abhängigkeit steht mit eigenen Kosten selbst da.
- Abgezogen werden nur die eigenen Kosten, nicht der Teilbaum.

Darstellung: Erledigte bleiben `cheap` und behalten ihre volle Statusfarbe
(grün/blau sagt schon „nichts mehr zu tun"), verlieren aber Stationspunkt und
implizites M-Badge — Letzteres macht eine Kostenannahme sichtbar, und an einem
erledigten Knoten wird keine getroffen. Station ist der tiefste noch OFFENE
Knoten eines Zweigs (`hidesOpenCheap()` fragt den Teilbaum statt nur die
direkten Kinder): Sind alle Kinder fertig, wird der offene Elternknoten selbst
die Station. Der eingeklappte Knoten (D38) erbt dieselbe Verschärfung.

Kein neuer Umschalter — der vorhandene ändert seine Bedeutung.

Nachgemessen am Werkbaum-Plan: 110 Knoten auf dem Pfad (unverändert),
Stationen 69 -> 24, und die 24 sind exakt die offene Front (Ticket-Referenzen,
Öffnen/Speichern, Backend-Gerüst samt REST und Persistenz, Websocket-Transport,
Text-CRDT, Mermaid-Layout, IDEA-Plugin). Linie wird weiter gezogen; Export und
Druck folgen ohne Zusatzcode, sie lesen dieselbe `.node.cheap-leaf`.

236 Tests grün, davon 14 neue (tests/frontier.test.js). Der Snapshot des
kanonischen Beispiels ändert sich um genau zwei Knoten.

SPEC §9, llms.md und ROADMAP/TASKS nachgezogen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-23 12:14:34 +02:00
co-authored by Claude Fable 5
parent 9069955895
commit c793bf0038
9 changed files with 342 additions and 36 deletions
+88
View File
@@ -2909,3 +2909,91 @@ auf dem Dev-Server ist das gleichgültig, weil dort HMR arbeitet.
| Server liefert anderen Commit | — | „✅ Neuer Build 2222222“, Banner + Footer-Symbol |
| Danach wieder derselbe Commit | Banner blieb stehen | Banner und Symbol verschwinden |
| „Später“, dann F5 | Banner sofort wieder da | weg und bleibt weg |
## D46 — Der günstigste Pfad zeigt die offene Front: `[x]` kostet nichts mehr
Der Pfad (D18/D42) rechnete rein aus T-Shirt-Größen — `ownCost()` war
`SIZE_RANK[size] + 1`, der **Status kam in der Kostenrechnung überhaupt nicht
vor** (nur `[-]` flog heraus). Zwei Folgen, beide falsch für die Frage, für die
man auf den Pfad schaut:
- Längst Erledigtes wurde voll eingepreist und lag weiter hell auf dem Pfad. Im
mitgelieferten Werkbaum-Plan zeichnete die Linie damit überwiegend **fertige
Arbeit** nach: 69 Stationen, fast alle auf `[^]`-Knoten.
- In einer Alternativgruppe hatte eine **bereits realisierte** Alternative
keinerlei Kostenvorteil. Steht in einer `=`-Gruppe eine auf `[x]` und daneben
eine billigere auf `[?]`, empfahl der Pfad die billigere — obwohl die Wahl
faktisch getroffen und bezahlt ist. Die `xorConflict`-Warnung (D35) meldete
den Widerspruch bereits, die Pfadrechnung ignorierte ihn.
**Entschieden: Erledigtes kostet 0.** Der Pfad beantwortet damit „was ist als
Nächstes am günstigsten?" statt „was hätte der Plan von vorn gekostet?".
**Die Schwelle liegt bei `[x]` fertig** (Nutzer). Begründung: Die Beförderung
auf `[^]` ist keine Kostenfrage — sie sagt etwas über das Deployment, und das
tut per D30 ohnehin ein eigener Commit. Wer fertig ist, hat bezahlt.
**Angefangenes (`[~]`, `[/]`) zählt weiterhin voll.** Erwogen und verworfen
waren anteilige Restkosten (etwa 2/3 bzw. 1/3): Die Bruchteile wären erfunden —
die Größen sind **ordinal, nicht additiv** (ROADMAP, Aufwands-Rollup: `S+S ≠ M`),
eine Skala für „ein Drittel von L" gibt es nicht. Ebenfalls verworfen: alles ab
`[~]` als bezahlt zu werten („versunkene Kosten sind versunken"). Das zöge den
Pfad in jeden angefangenen Zweig, auch wenn dort noch fast alles offen ist —
und `[~]` heißt laut §4 gerade „Kosten investiert, **Risiko hoch**".
**Maßgeblich ist der intrinsische Status, nicht der effektive.** Ein `[x]`, das
von einer Abhängigkeit zurückgehalten wird, ist effektiv weiter unten (D39) —
die **Arbeit daran** ist trotzdem getan. Dieselbe Linie, die schon die XOR-Regel
(D35: „investiert ist investiert") und „Was ist neu?" (D28: `[^]` im Text ist
die Deploy-Aussage) ziehen. Nebenbei verhindert es doppeltes Zählen: Die
Abhängigkeit steht mit ihren eigenen Kosten ohnehin selbst auf dem Pfad.
**Abgezogen werden nur die eigenen Kosten, nicht der Teilbaum.** Ein `[x]`-Knoten
mit offenen Kindern bleibt also teuer — was stimmt, und was eine unstimmige
Stelle im Plan sichtbar lässt, statt sie zuzudecken.
**Darstellung: Farbe bleibt, nur Linie und Punkte lassen Erledigtes aus**
(Nutzer). Erledigte Knoten bleiben `cheap` und behalten ihre volle Statusfarbe —
grün bzw. blau sagt bereits „hier ist nichts mehr zu tun". Verworfen war,
sie wie nicht benötigte Knoten auszublassen: Das ist der Kanal für „gehört nicht
zum Plan" (D18), und ein fertiger Knoten sähe damit aus wie eine verworfene
Alternative. Ebenfalls verworfen: eine **zweite, schwächere** Abblendstufe — sie
müsste sich von der Pfad-Inversion unterscheiden lassen, und der Unterschied
zwischen 32 % und, sagen wir, 65 % Deckkraft ist kein Unterschied in der Art.
Drei Zustände sind so ohne neuen Farbkanal unterscheidbar: nicht nötig (blass),
nötig und erledigt (voll, ohne Punkt), nötig und offen (voll, mit Punkt und
Linie).
**Station ist der tiefste noch OFFENE Knoten eines Zweigs.** Die alte Regel
(„kein Kind liegt auf dem Pfad") reicht dafür nicht: Sind alle Kinder erledigt
und der Elternknoten nicht, hätte der Zweig gar keine Station, obwohl dort noch
Arbeit liegt — nämlich seine. `hidesOpenCheap()` fragt deshalb den **Teilbaum**
statt nur die direkten Kinder. Der eingeklappte Knoten (D38-Nachtrag) erbt
dieselbe Verschärfung: Er vertritt seinen Teilbaum nur noch, solange darin etwas
offen ist; ein fertig zusammengefalteter Zweig bekommt keinen Punkt mehr.
**Das implizite M-Badge entfällt an erledigten Knoten.** Es macht eine
**Kostenannahme** sichtbar (D18) — wo keine getroffen wird, gibt es nichts zu
zeigen. Gilt für Badge und `aria-label` gemeinsam.
**Kein neuer Umschalter** (Nutzer). Der vorhandene „günstigster Pfad" ändert
seine Bedeutung; erwogen war ein zweiter Knopf „offene Front ⇄ Gesamtplan", der
auch „was kostet der Plan insgesamt?" beantwortet hätte. Verworfen: ein neunter
Knopf im Diagramm-Kopf plus i18n in neun Sprachen für eine Frage, die man beim
Planen selten stellt — und die Kopfzeile wurde gerade erst entlastet
(D17-Nachtrag 5).
**Nachgemessen am mitgelieferten Werkbaum-Plan:** 110 Knoten auf dem Pfad
(unverändert), Stationen **69 → 24**. Die verbleibenden sind exakt die offene
Front — Ticket-Referenzen, Öffnen/Speichern, Backend-Gerüst samt REST und
Persistenz, Websocket-Transport, Text-CRDT, die Mermaid-Layout-Teile, das
IDEA-Plugin. Die Linie wird weiterhin gezogen (zwei Pfade: kräftig hinten,
abgetönt vorn). Grafikexport und Druck folgen ohne Zusatzcode — beide lesen
`.node.cheap-leaf` aus dem DOM. 236 Tests grün, davon 14 neue
(`tests/frontier.test.js`); der Snapshot des kanonischen Beispiels ändert sich
um genau zwei Knoten (`[x] Zielgruppenanalyse`, `[x] Sitemap` verlieren
`cheap-leaf`).
**Was das für den gestaffelten Pfad aus der ROADMAP bedeutet:** Dessen
Status-Hälfte ist damit gebaut. Offen bleibt die **Nutzen-Achse** (Ausbaustufen:
ist eine Gruppe komplett realisiert, zur nächsten per Nutzen gewählten Stufe
weiterspringen) — die braucht erst ein Nutzen-Attribut und den Aufwands-Rollup.
+7 -8
View File
@@ -95,14 +95,13 @@ Tastendruck ein Commit); und wer bei einem Backend eigentlich was darf
reiner Kostenminimierung. Rollt analog zum Aufwands-Rollup hoch.
(Syntax offen — SPEC-first; `(…)`/`@`/`#`/`%%` sind belegt, es braucht ein
eigenes kollisionsfreies Zeichen.)
- Gestaffelter „günstigster Pfad" (Ausbaustufen): die Pfad-Rechnung (D18)
status-bewusst machen — `fertig`/`prod` gelten als erledigt (Hintergrund),
hervorgehoben wird nur der günstigste noch **offene** Rest (aktuelle Front).
Ist eine Alternativen-Gruppe komplett realisiert, springt der Pfad zur
nächsten, per Nutzen gewählten Ausbaustufe weiter (Regel: erst die Kette auf
`fertig`, dann die nächste Stufe). Baut auf Nutzen + Aufwands-Rollup auf und
erweitert D18 (heute rein statisch/kostenminimierend) um Nutzen- und
Status-/Zeitachse.
- Gestaffelter „günstigster Pfad" (Ausbaustufen) — **die Status-Hälfte ist
gebaut** (D46): `fertig`/`prod` kosten nichts mehr, hervorgehoben wird der
günstigste noch **offene** Rest (aktuelle Front); eine realisierte Alternative
gewinnt ihre Gruppe. Offen bleibt die **Nutzen-Achse**: Ist eine
Alternativen-Gruppe komplett realisiert, zur nächsten, per Nutzen gewählten
Ausbaustufe weiterspringen (Regel: erst die Kette auf `fertig`, dann die
nächste Stufe). Das braucht erst Nutzen + Aufwands-Rollup (oben).
- Attribut-Syntax für Termine/Meilensteine.
- Schlagworte `&tag` mit einer Filter-/Hervorheben-Linse im Diagramm: Der Baum
ist genau eine Zerlegung — ein Tag benennt eine Menge von Knoten quer dazu
+31 -11
View File
@@ -459,9 +459,19 @@ Voreinstellung auf kleinem Bildschirm ist **Vollbild** (siehe D17).
### Günstigster Pfad (Kosten-Hervorhebung)
Ein Umschalter (Icon-Button im Diagramm-Kopf, Voreinstellung **an**, Zustand
persistiert) hebt den kostengünstigsten Weg durch den Baum hervor. Ermittelt
persistiert) hebt den kostengünstigsten Weg durch den Baum hervor. Gerechnet
wird die **noch offene** Arbeit — der Pfad beantwortet „was ist als Nächstes
am günstigsten?", nicht „was hätte der Plan von vorn gekostet?". Ermittelt
werden die für die günstigste Realisierung **nötigen** Knoten:
- **Erledigtes kostet nichts mehr:** Ein Knoten mit `[x]` oder `[^]` (§4) geht
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
**intrinsische** Status: Investiert ist investiert, auch wenn Abhängigkeiten
den Knoten effektiv zurückhalten (§4). Abgezogen werden nur die **eigenen**
Kosten des Knotens, nicht die seines Teilbaums. Folge in 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.
- **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.
@@ -497,20 +507,27 @@ Darstellung per **Inversion**: nicht benötigte Knoten (nicht-gewählte
any-of-Alternativen und optionale Knoten, je samt Teilbaum) treten zurück
(blass, entsättigt); der
günstige Pfad hebt sich dadurch von selbst ab — kein zusätzlicher Rahmen an den
ohnehin dichten Knoten-Ecken. Wo die Größe **implizit** als `M` angenommen wird,
ohnehin dichten Knoten-Ecken. **Erledigte Knoten treten nicht zurück**: Sie
gehören zum Pfad und behalten ihre volle Statusfarbe (§4) — grün bzw. blau sagt
bereits „hier ist nichts mehr zu tun"; sie auszublassen hieße, sie sähen aus wie
eine verworfene Alternative. Wo die Größe **implizit** als `M` angenommen wird,
zeigt der Knoten ein **invertiertes** Größen-Badge (weiß mit petrolfarbenem
Rand/Text statt gefüllt) mit erläuterndem Tooltip.
Rand/Text statt gefüllt) mit erläuterndem Tooltip — an einem **erledigten**
Knoten entfällt es, dort wird keine Kostenannahme mehr getroffen.
Zusätzlich fädelt eine **gestrichelte, geschwungene Petrol-Linie** durch die
**Endknoten (Blätter)** des Pfads (Katmull-Rom-Spline in Dokument-Reihenfolge,
**offenen Endknoten** des Pfads (Katmull-Rom-Spline in Dokument-Reihenfolge,
in allen Modi). Die kräftige Linie liegt **hinter** den Knoten (nur in den
Lücken voll sichtbar), eine **abgetönte Kopie** davor deutet den Verlauf beim
Durchschreiten eines Knotens nur schwach an. An jedem echten Endknoten sitzt ein
großer, **blasser Petrol-Stationspunkt** (U-Bahn-Plan-Prinzip: der Knotentext
bleibt lesbar) — nur **durchquerte** Fremd- oder Zwischenknoten tragen keinen
Punkt, sodass eindeutig bleibt, welche Knoten auf dem Pfad **enden**. Linie,
Durchschreiten eines Knotens nur schwach an. An jedem solchen Endknoten sitzt
ein großer, **blasser Petrol-Stationspunkt** (U-Bahn-Plan-Prinzip: der
Knotentext bleibt lesbar) — **durchquerte** Fremd- oder Zwischenknoten und
**erledigte** Knoten tragen keinen, sodass eindeutig bleibt, wo noch Arbeit
**endet**. Station ist damit der tiefste noch offene Knoten eines Zweigs: Sind
alle Kinder erledigt, wird der offene Elternknoten selbst die Station (die
Restarbeit ist dann seine); ist ein ganzer Zweig erledigt, hat er keine. Linie,
abgetönte Kopie und Punkte erscheinen bei aktivem Umschalter auch im
Grafikexport. Siehe D18.
Grafikexport. Siehe D18, D46.
### Diagramm aus einer URL laden (`?sourceUrl=`)
Der Editor kann den Notationstext aus einer externen Textdatei beziehen:
@@ -723,10 +740,13 @@ fokussierten Knoten (WAI-ARIA-Baum-Idiom).
weiter gemeldet (sie gelten dem Text), und der günstigste Pfad rechnet
unverändert über den ganzen Baum.
- **Ein eingeklappter Knoten vertritt seinen Teilbaum auch auf dem günstigsten
Pfad:** Liegt darin etwas auf dem Pfad, ist er dessen tiefste noch sichtbare
Pfad:** Liegt darin noch **offene** Pfadarbeit (§9, Erledigtes zählt nicht),
ist er deren tiefste noch sichtbare
Station — die Linie führt zu ihm und endet dort mit einem Stationspunkt
(„hier drin liegt noch Pfad"). Ohne das überspränge sie den ganzen Zweig, als
wäre dort nichts zu tun. Beim Aufklappen geben die Station wieder die
wäre dort nichts zu tun. Ist der verborgene Teilbaum dagegen **fertig**,
bekommt der eingeklappte Knoten keinen Punkt — dort ist wirklich nichts mehr
zu tun. Beim Aufklappen geben die Station wieder die
Endknoten darunter. Das gilt auch, wenn der eingeklappte Knoten selbst nicht
gebraucht wird, sein Teilbaum aber schon (etwa ein per `:#…` gezogenes Ziel):
Er ist dann der einzige sichtbare Griff auf nötige Arbeit und tritt deshalb
+16
View File
@@ -126,3 +126,19 @@ entscheiden, **bevor** Code entsteht.
einem `---`-Trenner (SPEC §11). Umgesetzt (D40): Parser (`desc` am
Knoten, Warnungen `unknownDesc`/`descStray`), Anzeige im Tooltip +
`aria-label` mit ”-Marke am Knoten; `tests/desc.test.js`.
## Phase 5 — Der Pfad zeigt, was als Nächstes dran ist
- [x] Günstigsten Pfad status-bewusst rechnen: Erledigtes (`[x]`/`[^]`) kostet
nichts mehr, hervorgehoben wird die offene Front.
→ Umgesetzt (D46): `isDone()` + `ownCost()` in model.js; Stationen sind
die tiefsten noch **offenen** Pfadknoten (`hidesOpenCheap()`), erledigte
behalten Farbe und Pfadzugehörigkeit, verlieren aber Punkt und implizites
M-Badge. Angefangenes (`[~]`/`[/]`) zählt weiter voll, maßgeblich ist der
intrinsische Status. Kein neuer Umschalter — der vorhandene ändert seine
Bedeutung. `tests/frontier.test.js`.
- [ ] Nutzen je Knoten (Gegenstück zur Kosten-Größe) — Schreibweise offen,
SPEC-first (siehe ROADMAP „Kleinere Ideen").
- [ ] Aufwands-Rollup (Größen der Kinder aggregieren) — braucht erst eine
Mapping-Konvention, Größen sind ordinal.
- [ ] Ausbaustufen: ist eine Alternativgruppe komplett realisiert, zur nächsten
per Nutzen gewählten Stufe weiterspringen. Baut auf beidem oben auf.
+7
View File
@@ -74,6 +74,13 @@ One node per line. Everything except the label is optional.
- No box = neutral node. `x` may also be uppercase `X`.
- Unknown codes are tolerated: the node renders neutral, with a warning —
the line is never lost.
- **`[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.
### Size (effort)
+40 -13
View File
@@ -49,8 +49,19 @@ export function pathChildren(n){
return n.children.filter(k =>
!k.optional && (!k.status || k.status.key !== 'verworfen'));
}
/* fehlende Größe wird als M interpretiert */
export function ownCost(n){ return SIZE_RANK[n.size || 'M'] + 1; }
/* ---------- Erledigt: was nichts mehr kostet (SPEC §9, D46) ----------
`[x]` fertig und `[^]` in Produktion. Die Beförderung auf Prod ist keine
Kostenfrage (D30: das tut der Deploy), fertig ist also die Schwelle.
Angefangenes (`[~]`, `[/]`) zählt bewusst voll: Die Arbeit ist noch offen,
und Bruchteile ordinaler T-Shirt-Größen wären erfunden.
Maßgeblich ist der INTRINSISCHE Status investiert ist investiert, auch
wenn Abhängigkeiten den Knoten effektiv zurückhalten (D39); deren eigene
Kosten stehen ohnehin an ihnen selbst. */
export function isDone(n){
return !!n.status && (n.status.key === 'fertig' || n.status.key === 'prod');
}
/* 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);
@@ -185,19 +196,35 @@ export function hidesCheap(n, cheapSet){
}
return false;
}
/* Dasselbe, aber nur für noch OFFENE Pfadarbeit (D46): Erledigtes zählt nicht
mehr, es ist keine Station und hält auch keine unter sich. */
export function hidesOpenCheap(n, cheapSet){
for(const k of n.children || []){
if((cheapSet.has(k) && !isDone(k)) || hidesOpenCheap(k, cheapSet)) return true;
}
return false;
}
export function cheapCls(n, cheapSet, collapsed){
/* Eingeklappt steht der Knoten stellvertretend für seinen ganzen Teilbaum
(SPEC §9/D38): Liegt darin etwas auf dem Pfad, ist er dessen tiefste noch
SICHTBARE Station sonst überspränge die Linie den Zweig, als wäre dort
nichts zu tun. Das gilt auch, wenn er selbst nicht gebraucht wird, sein
Teilbaum aber schon (eine per `:#…` gezogene Alternative, D42): Er ist
dann der einzige sichtbare Griff auf nötige Arbeit und darf deshalb auch
nicht von der Inversion ausgeblasst werden. */
if(collapsed) return (cheapSet.has(n) || hidesCheap(n, cheapSet)) ? 'cheap cheap-leaf' : '';
if(!cheapSet.has(n)) return '';
const leaf = !pathChildren(n).some(k => cheapSet.has(k));
return leaf ? 'cheap cheap-leaf' : 'cheap';
/* Stationen sind die tiefsten noch OFFENEN Knoten des Pfads (D46). Ein
erledigter Knoten bleibt `cheap` er gehört zum Pfad und behält seine
volle Statusfarbe , trägt aber keinen Punkt und wird von der Linie
übergangen; die zeigt den günstigsten noch zu gehenden Rest.
Eingeklappt steht der Knoten stellvertretend für seinen ganzen Teilbaum
(SPEC §9/D38): Liegt darin noch offene Pfadarbeit, ist er deren tiefste
noch SICHTBARE Station sonst überspränge die Linie den Zweig, als wäre
dort nichts zu tun. Das gilt auch, wenn er selbst nicht gebraucht wird,
sein Teilbaum aber schon (eine per `:#…` gezogene Alternative, D42): Er
ist dann der einzige sichtbare Griff auf nötige Arbeit und darf deshalb
auch nicht von der Inversion ausgeblasst werden. */
const onPath = cheapSet.has(n);
if(collapsed){
if((onPath && !isDone(n)) || hidesOpenCheap(n, cheapSet)) return 'cheap cheap-leaf';
return (onPath || hidesCheap(n, cheapSet)) ? 'cheap' : '';
}
if(!onPath) return '';
if(isDone(n)) return 'cheap';
return hidesOpenCheap(n, cheapSet) ? 'cheap' : 'cheap cheap-leaf';
}
/* ---------- Effektiver Status (SPEC §4/§9, D39) ----------
+6 -3
View File
@@ -15,7 +15,7 @@
// Diskrepanzen (effectiveStatus() in model.js, D39)
} */
import { gateOf, needsBreakdown, visibleChildren, cheapCls } from './model.js';
import { gateOf, needsBreakdown, visibleChildren, cheapCls, isDone } from './model.js';
/* Zusatzklassen eines Knotens: günstigster Pfad (D18), neu in Produktion"
gegenüber der zuletzt gesehenen Fassung (D28, `freshSet` optional) und
@@ -68,7 +68,7 @@ function nodeAria(n, opts, fold){
const effKey = opts.effStatus ? opts.effStatus.get(n) : undefined;
if(effKey) parts.push(t('a11yEffective', {status: t('st_' + effKey)}));
if(n.size) parts.push(t('a11ySize', {size: n.size}));
else if(cheapPath) parts.push(t('a11ySizeImplicit'));
else if(cheapPath && !isDone(n)) parts.push(t('a11ySizeImplicit'));
if(n.tags && n.tags.length) parts.push(t('a11yTags', {names: n.tags.join(', ')}));
/* Knoten-ID und Abhängigkeiten (SPEC §1, D36/D37): keine eigene Darstellung
im Diagramm sichtbar nur im Tooltip und hier. */
@@ -131,10 +131,13 @@ function nodeHtml(n, extra, opts, fold){
const tagsHtml = n.tags && n.tags.length
? `<span class="tags" aria-hidden="true">${n.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('')}</span>`
: '';
/* Das implizite M-Badge macht eine KOSTENANNAHME sichtbar (D18). An einem
erledigten Knoten wird keine getroffen er kostet nichts mehr (D46) ,
dort bleibt es deshalb weg. */
const implicitTip = attr(t('implicitSizeTooltip'));
const sizeBadge = n.size
? `<span class="size" aria-hidden="true">${n.size}</span>`
: (cheapPath ? `<span class="size implicit" aria-hidden="true" title="${implicitTip}">M</span>` : '');
: (cheapPath && !isDone(n) ? `<span class="size implicit" aria-hidden="true" title="${implicitTip}">M</span>` : '');
/* High-Risk: Warndreieck (, Textpräsentation via VS15) an der oberen linken
Ecke. aria-hidden die Information steckt bereits im Status des aria-label. */
const riskMark = n.status && n.status.key === 'highrisk'
@@ -2,6 +2,6 @@
exports[`renderTreeHtml — kanonisches Beispiel > Grundzustand (Pfad aus, verworfene aus): Struktur-Snapshot 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" aria-expanded="true" title="st_fertig · jumpHint"><span class="fold" aria-hidden="true">▾</span>Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" aria-expanded="true" title="st_geplant · jumpHint"><span class="fold" aria-hidden="true">▾</span>CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus" title="st_geplant · jumpHint">WordPress</div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus" title="st_idee · jumpHint">Headless CMS</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" aria-expanded="true" title="st_idee · jumpHint"><span class="fold" aria-hidden="true">▾</span>Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" data-line="15" aria-label="Cloud" title="jumpHint">Cloud</div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise" title="jumpHint">On-Premise</div></li></ul></li></ul></li>"`;
exports[`renderTreeHtml — kanonisches Beispiel > günstigster Pfad an: cheap/cheap-leaf + implizite M-Badges 1`] = `"<li class="has-and"><a class="node root-node cheap st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node cheap st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" aria-expanded="true" title="st_fertig · jumpHint"><span class="fold" aria-hidden="true">▾</span>Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node cheap cheap-leaf st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node cheap cheap-leaf st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node cheap st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node cheap cheap-leaf st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node cheap st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" aria-expanded="true" title="st_geplant · jumpHint"><span class="fold" aria-hidden="true">▾</span>CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus, a11ySizeImplicit" title="st_geplant · jumpHint">WordPress<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus, a11ySizeImplicit" title="st_idee · jumpHint">Headless CMS<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li><li class="has-or"><div class="node cheap st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" aria-expanded="true" title="st_idee · jumpHint"><span class="fold" aria-hidden="true">▾</span>Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf" tabindex="0" data-line="15" aria-label="Cloud, a11ySizeImplicit" title="jumpHint">Cloud<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise, a11ySizeImplicit" title="jumpHint">On-Premise<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li>"`;
exports[`renderTreeHtml — kanonisches Beispiel > günstigster Pfad an: cheap/cheap-leaf + implizite M-Badges 1`] = `"<li class="has-and"><a class="node root-node cheap st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node cheap st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" aria-expanded="true" title="st_fertig · jumpHint"><span class="fold" aria-hidden="true">▾</span>Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node cheap st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node cheap st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node cheap st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node cheap cheap-leaf st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node cheap st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" aria-expanded="true" title="st_geplant · jumpHint"><span class="fold" aria-hidden="true">▾</span>CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus, a11ySizeImplicit" title="st_geplant · jumpHint">WordPress<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus, a11ySizeImplicit" title="st_idee · jumpHint">Headless CMS<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li><li class="has-or"><div class="node cheap st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" aria-expanded="true" title="st_idee · jumpHint"><span class="fold" aria-hidden="true">▾</span>Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf" tabindex="0" data-line="15" aria-label="Cloud, a11ySizeImplicit" title="jumpHint">Cloud<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise, a11ySizeImplicit" title="jumpHint">On-Premise<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li>"`;
exports[`renderTreeHtml — kanonisches Beispiel > verworfene einblenden: Eigenentwicklung erscheint (durchgestrichen) 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" aria-expanded="true" title="st_fertig · jumpHint"><span class="fold" aria-hidden="true">▾</span>Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" aria-expanded="true" title="st_geplant · jumpHint"><span class="fold" aria-hidden="true">▾</span>CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus" title="st_geplant · jumpHint">WordPress</div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus" title="st_idee · jumpHint">Headless CMS</div></li><li><div class="node st-verworfen" tabindex="0" data-line="13" aria-label="Eigenentwicklung, a11yStatus" title="st_verworfen · jumpHint">Eigenentwicklung</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" aria-expanded="true" title="st_idee · jumpHint"><span class="fold" aria-hidden="true">▾</span>Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" data-line="15" aria-label="Cloud" title="jumpHint">Cloud</div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise" title="jumpHint">On-Premise</div></li></ul></li></ul></li>"`;
+146
View File
@@ -0,0 +1,146 @@
import { describe, it, expect } from 'vitest';
import { parse } from '../src/parser.js';
import {
isDone, ownCost, cheapestCost, cheapCls, computeCheapSet,
} from '../src/model.js';
/* Status-bewusster günstigster Pfad (SPEC §9, D46): Erledigtes kostet nichts
mehr und ist keine Station; hervorgehoben wird der günstigste noch OFFENE
Rest die aktuelle Front. */
const roots = txt => parse(txt).roots;
const cheapLabels = txt => [...computeCheapSet(roots(txt))].map(n => n.label).sort();
/* Stationen = Knoten mit 'cheap-leaf' (die Pfadlinie fädelt durch sie). Der
Baum wird EINMAL geparst und beides daraus abgeleitet `Set.has()` prüft
auf Objektidentität, ein zweiter Parse-Durchlauf träfe nie (Falle aus D28). */
function plan(txt){
const rs = roots(txt);
const set = computeCheapSet(rs);
const st = [];
(function walk(ns){
for(const n of ns){
if(cheapCls(n, set, false).includes('cheap-leaf')) st.push(n.label);
walk(n.children);
}
})(rs);
return { rs, set, stationen: st.sort(), aufPfad: [...set].map(n => n.label).sort() };
}
describe('isDone — die Schwelle liegt bei „fertig"', () => {
it('erkennt [x] und [^], sonst nichts', () => {
const rs = roots(`[?] a
[ ] b
[!] c
[~] d
[/] e
[x] f
[^] g
[-] h
i`);
expect(rs.map(isDone)).toEqual([false, false, false, false, false, true, true, false, false]);
});
});
describe('Kosten — Erledigtes zählt nicht mehr', () => {
it('kostet 0, unabhängig von der Größe', () => {
const [a, b, c] = roots(`[x] Fertig (XXL)
[^] Prod (XL)
[~] Arbeit (XS)`);
expect(ownCost(a)).toBe(0);
expect(ownCost(b)).toBe(0);
expect(ownCost(c)).toBeGreaterThan(0);
});
it('Angefangenes ([~], [/]) zählt weiterhin voll', () => {
const [a, b] = roots(`[~] A (L)
[ ] B (L)`);
expect(ownCost(a)).toBe(ownCost(b));
});
it('zählt den intrinsischen Status — Abhängigkeiten halten ihn nicht auf', () => {
/* Effektiv ist A erst „geplant" (D39), investiert ist trotzdem investiert. */
const [a] = roots(`[x] A (XL) :#b
[ ] B #b (XS)`);
expect(ownCost(a)).toBe(0);
});
it('zieht nur die eigenen Kosten ab, nicht den Teilbaum', () => {
const [n] = roots(`[x] Eltern (L)
- [ ] Kind (S)`);
expect(cheapestCost(n)).toBe(cheapestCost(roots('[ ] Kind (S)')[0]));
});
});
describe('Auswahl — die getroffene Wahl gewinnt', () => {
it('eine realisierte Alternative schlägt die nominell billigere', () => {
/* Ohne Status-Bewusstsein gewönne „Billig" (XS gegen L). */
expect(cheapLabels(`[ ] Wahl (XS)
| [x] Gemacht (L)
| [ ] Billig (XS)`)).toEqual(['Gemacht', 'Wahl']);
});
it('eine erst angefangene Alternative gewinnt dadurch NICHT', () => {
expect(cheapLabels(`[ ] Wahl (XS)
| [~] Angefangen (L)
| [ ] Billig (XS)`)).toEqual(['Billig', 'Wahl']);
});
it('gilt auch in einer XOR-Gruppe', () => {
expect(cheapLabels(`[ ] Wahl (XS)
= [ ] Billig (XS)
= [^] Live (XL)`)).toEqual(['Live', 'Wahl']);
});
});
describe('Stationen — die Linie zeigt den offenen Rest', () => {
it('erledigte Blätter tragen keinen Punkt', () => {
const p = plan(`[ ] W (XS)
- [x] Fertig (S)
- [ ] Offen (S)`);
expect(p.aufPfad).toEqual(['Fertig', 'Offen', 'W']); /* beide bleiben auf dem Pfad */
expect(p.stationen).toEqual(['Offen']);
});
it('ein offener Knoten mit lauter erledigten Kindern wird selbst zur Station', () => {
/* Die Restarbeit ist dann die des Elternknotens sonst hätte der Zweig
gar keine Station, obwohl dort noch etwas offen ist. */
const p = plan(`[~] Eltern (M)
- [x] A (S)
- [^] B (S)`);
expect(p.stationen).toEqual(['Eltern']);
});
it('ein erledigter Knoten mit offenem Kind bleibt Durchgang', () => {
const p = plan(`[x] Eltern (M)
- [ ] Kind (S)`);
expect(p.stationen).toEqual(['Kind']);
});
it('ein durchweg erledigter Baum hat keine Station mehr', () => {
const p = plan(`[^] W (L)
- [x] A (S)
- [x] B (S)`);
expect(p.stationen).toEqual([]);
expect(p.aufPfad).toEqual(['A', 'B', 'W']); /* der Pfad selbst bleibt */
});
});
describe('Eingeklappt — der Knoten vertritt offene Arbeit, nicht erledigte', () => {
it('vertritt einen Teilbaum mit offener Pfadarbeit als Station (D38)', () => {
const rs = roots(`[ ] W (XS)
- [ ] Zweig (M)
- [ ] Tief (S)`);
const set = computeCheapSet(rs);
const zweig = rs[0].children[0];
expect(cheapCls(zweig, set, true)).toBe('cheap cheap-leaf');
});
it('bleibt ohne Punkt, wenn darunter alles erledigt ist', () => {
const rs = roots(`[ ] W (XS)
- [x] Zweig (M)
- [x] Tief (S)`);
const set = computeCheapSet(rs);
const zweig = rs[0].children[0];
expect(cheapCls(zweig, set, true)).toBe('cheap'); /* auf dem Pfad, aber nichts zu tun */
});
});