feat: Fehlende Größe aus den Teilpaketen geschätzt statt pauschal M (D66)

Mindestens die größte Größe der zählenden Kinder, ab drei Kindern dieser
Größe eine Stufe mehr (Deckel XXL); disjunktiv die kleinste Alternative,
rekursiv über größenlose Kinder, Blatt bleibt M. Badge, Tooltip und
aria-label zeigen die geschätzte Größe („mindestens {size} angenommen").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-24 19:59:13 +02:00
co-authored by Claude Fable 5
parent ebc5be407e
commit bb01045488
8 changed files with 317 additions and 43 deletions
+1
View File
@@ -19,6 +19,7 @@ reverse.
## 2026-08-24
- A missing size is now estimated from the sub-packages — at least the largest child size, one step more from three children of that size
- Node titles wrap at thirty-two characters now instead of forty — long titles pulled the diagram wide
- Fix: in the vertical layout, the line from a parent to one or two children was torn — the collector rail now always reaches the parent's stub
- The legend shows the high-risk status `[!]` and mentions the size check
+62
View File
@@ -4928,3 +4928,65 @@ bleiben unberührt. Sichtbare Folge: mehr Knoten brechen um (rund 100 Zeichen
ergeben jetzt vier statt drei Zeilen), dafür wird der breiteste Fächer
schmaler. Testerwartungen mit dem echten Algorithmus nachgerechnet statt
geschätzt.
## D66 — Fehlende Größe wird aus den Teilpaketen geschätzt statt pauschal M
Gewünscht vom Nutzer: „Die Kostenschätzung für Knoten ohne explizite
T-Shirt-Größe soll anhand der Sub-Knoten geschehen. Mindestens die größte
T-Shirt-Größe der Sub-Knoten; wenn mehr als 2 diese Größe haben, also ab 3,
dann sogar eine T-Shirt-Größe mehr." Die alte D18-Pauschale — fehlende Größe
= M — unterschätzte jeden größenlosen Sammelknoten, sobald ein Kind über M
lag; die Kinder sagen mehr, als die Pauschale nutzte.
**Die Regel:** Angenommen wird **mindestens die größte Größe der zählenden
Kinder**; tragen **drei oder mehr** Kinder diese größte Größe, eine Stufe
mehr (Deckel `XXL`). Ein Knoten ohne Größe und ohne zählende Kinder bleibt
beim M-Rückfall — für ein Blatt gibt es nichts abzuleiten, und D18 nannte M
selbst schon „die konservative Annahme mindestens M'".
**Es zählen dieselben Kinder wie beim Größen-Konflikt (§5/D62)** — direkte,
verworfene und optionale (`+`) nie, in einer disjunktiven Gruppe (`|`/`=`)
nur die **kleinste** Alternative —, mit genau einem Unterschied: Kinder
**ohne** Größe zählen hier mit, ihre Größe wird nach derselben Regel
**rekursiv** mitgeschätzt. D62 schließt sie aus, weil eine fehlende Größe
keine Autoren-Aussage ist und die Konfliktprüfung nur meldet, was sicher
ist; hier wird ohnehin geschätzt — sie auszuschließen hieße, drei größenlose
Blätter für kostenlos zu halten. Die D62-Zählung zu übernehmen statt eine
dritte Regelmenge zu erfinden hält die Doku bei einem Satz: „dieselben
Kinder wie beim Größen-Konflikt".
**Disjunktiv gilt das Minimum, und keine Stufe mehr.** Realisiert wird genau
eine Alternative — die kleinste ist der Boden, den jede Wahl mindestens
kostet, und drei gleich große Alternativen sind kein dreifacher Aufwand. Ist
in der Gruppe etwas realisiert, ist die Wahl getroffen (D61, `chosenPool`):
Dann zählt die kleinste der **realisierten**. Bewusst **nicht** die
Alternative, die der günstigste Pfad wählt — die Wahl hängt von den Kosten
ab und die Kosten hingen dann von der Wahl: ein Zirkel. Das Minimum ist
deterministisch und für ein „mindestens" die ehrliche Untergrenze.
**Nur die Kostenschätzung ändert sich.** Die §5-Semantik bleibt unberührt,
die Konfliktprüfung (D62) rechnet weiter nur mit angegebenen Größen, und die
Falt-Voreinstellung „ab M abwärts" (D44, `atMostM`) klappt weiterhin nichts
ohne Größenangabe zu — dort wäre die Schätzung eine Vermutung, die wie eine
Angabe behandelt würde (die D44-Begründung gilt wörtlich weiter).
**Das invertierte Badge zeigt jetzt die geschätzte Größe** statt immer „M";
Tooltip und `aria-label` sagen „mindestens {size} angenommen". Die beiden
vorhandenen i18n-Schlüssel wurden **parameterisiert** statt verdoppelt: Ein
Text mit „mindestens {size}" stimmt für die Ableitung wie für den
M-Rückfall — kein neuer Schlüssel in neun Sprachen für dieselbe Aussage.
**Memoisiert per WeakMap**, nicht am Knotenobjekt und nicht ungecacht:
`computeCheapPlan` (D42) ruft `ownCost` je Suchbelegung über die ganze
nötige Menge — eine ungecachte Rekursion wäre O(n²) je Belegung, bei 20 000
Belegungen zu viel. Der Parse-Baum wird bei jedem Tastendruck neu gebaut,
der Cache kann also nie veralten, und die WeakMap gibt alte Bäume von
selbst frei.
**Nachgemessen:** Der mitgelieferte Plan hat genau **einen** größenlosen
Knoten (`#not.people`, ein Blatt → weiterhin M) — Pfadknoten 135 und
Stationen 27 unverändert, 0 Warnungen. Das kanonische Beispiel (§10) ändert
sich nicht (seine größenlosen Knoten sind Blätter); alle Snapshots bleiben
stehen. 424 Tests, davon 19 neue in `tests/assumed.test.js`. Gegenprobe per
Mutation: Stufe-ab-drei entfernt → genau die drei danach benannten
Zusicherungen fallen; disjunktiv Maximum statt Minimum → genau eine; alte
M-Pauschale in `ownCost` zurückgebaut → genau die zwei Kosten-Tests.
+17 -7
View File
@@ -603,8 +603,17 @@ werden die für die günstigste Realisierung **nötigen** Knoten:
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
SPEC-Semantik der Größen in §5 bleibt unberührt).
- **Fehlende Größe wird aus den Teilpaketen geschätzt** (nur für diese
Kostenschätzung; die SPEC-Semantik der Größen in §5 bleibt unberührt):
Angenommen wird **mindestens die größte Größe der zählenden Kinder**;
tragen **drei oder mehr** Kinder diese größte Größe, eine Stufe mehr
(Deckel: `XXL`). Es zählen dieselben Kinder wie beim Größen-Konflikt (§5) —
nur die direkten, verworfene und optionale (`+`) nie, in einer disjunktiven
Gruppe (`|`/`=`) nur die **kleinste** Alternative (ist dort etwas realisiert,
§3, die kleinste der realisierten) —, mit einem Unterschied: Kinder **ohne**
Größe zählen hier mit, ihre Größe wird nach derselben Regel **rekursiv**
mitgeschätzt — geschätzt wird ohnehin. Ein Knoten ohne Größe und ohne
zählende Kinder wird weiterhin als `M` gewertet.
**Mit Abhängigkeiten (§1) zählt die Dependency Closure.** Jeder nötige Knoten
zieht seine `:#…`-Ziele samt deren Realisierung in die nötige Menge; gemeinsam
@@ -637,10 +646,11 @@ Alternative behalten ihre Farbe. Blass heißt damit einheitlich „hier ist nich
getan und wird auch nichts getan". Maßgeblich ist der **intrinsische** Status
(die eigene Box), gefärbt wird weiterhin nach dem effektiven (oben) — ein von
Abhängigkeiten zurückgehaltenes `[x]` steht also in voller Stärke in seiner
effektiven Farbe. 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 — an einem **erledigten**
Knoten entfällt es, dort wird keine Kostenannahme mehr getroffen.
effektiven Farbe. Wo die Größe **implizit** angenommen wird (aus den
Teilpaketen geschätzt bzw. `M`, oben), zeigt der Knoten die angenommene Größe
als **invertiertes** Größen-Badge (weiß mit petrolfarbenem 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
**offenen Endknoten** des Pfads (Katmull-Rom-Spline in Dokument-Reihenfolge,
@@ -1070,7 +1080,7 @@ Das Diagramm wird aus der Live-Geometrie in ein eigenständiges SVG (nur Formen
### Barrierefreiheit
Die visuell codierten Knoten-Eigenschaften werden für Screenreader in einem
sprechenden **`aria-label`** je Knoten zusammengefasst — Label, Status, Aufwand
(inkl. „(angenommen)“ beim impliziten M), Zuständige, ob der Knoten optional
(inkl. „(angenommen)“ bei implizit geschätzter Größe), Zuständige, ob der Knoten optional
(§3) und ob er verlinkt ist —, alles in der aktuellen UI-Sprache. Die rein visuellen Beiwerke
(Größen-Badge, Tags, ↗-Pfeil) sind `aria-hidden`, damit sie nicht kryptisch
doppelt vorgelesen werden. **Alle** Knoten sind fokussierbar (`tabindex="0"`
+5 -1
View File
@@ -108,7 +108,11 @@ One node per line. Everything except the label is optional.
- T-shirt sizes in parentheses: `(XS) (S) (M) (L) (XL) (XXL)`.
- From `(M)` upward a node **should be decomposed further**; a node ≥ M
without children gets a placeholder hint in the diagram.
- For cost estimation a missing size counts as `M`.
- For cost estimation a missing size is **estimated from the sub-packages**:
at least the largest size among the counting children (same children as
the conflict check below, except unsized children count too — estimated
recursively); three or more children at that largest size raise it by one
step, capped at `XXL`. Without counting children it stays `M`.
- **Size conflict check:** a given size must fit the direct children. For
this one check each size is read as a range (lower bounds double, XXL is
open-ended):
+18 -18
View File
@@ -2146,7 +2146,7 @@ const I18N = {
cheapTooltip:"Günstigsten Pfad hervorheben nicht benötigte Alternativen treten zurück",
leanNextTooltip:"Zur nächsten Station des günstigsten Pfads springen was als Nächstes dran ist ({n} offen)",
foldSmallTooltip:"Knoten der Größe M und kleiner zuklappen erneut drücken klappt alle wieder auf",
implicitSizeTooltip:"Keine Größe angegeben für die Kostenschätzung als M angenommen",
implicitSizeTooltip:"Keine Größe angegeben für die Kostenschätzung mindestens {size} angenommen",
fullscreenTooltip:"Vollbild Panels nutzen die ganze Fensterbreite",
brandTooltip:"„Werkbaum“ bedeutet so viel wie Werk-Baum — der Baum des Projektstrukturplans (WBS).",
editorTitle:"Struktur (Text)", diagramTitle:"Diagramm",
@@ -2202,7 +2202,7 @@ const I18N = {
unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.",
sourceLoadWarn:"„{url}“ konnte nicht geladen werden ({error}). Die Datei muss per http(s) erreichbar sein und CORS erlauben (Access-Control-Allow-Origin).",
sourceTimeoutWarn:"„{url}“ hat innerhalb von {seconds} s nicht geantwortet — der Abruf wurde abgebrochen. Etherpad begrenzt, wie oft der Export geholt werden darf (serienmäßig 10-mal pro 90 s); warte einen Moment und lade dann erneut.",
a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: M (angenommen)", a11yTags:"Zuständig: {names}", a11yId:"ID: #{id}", a11yDeps:"hängt ab von: {ids}", a11yFolded:"eingeklappt, {n} verborgen", a11yEffective:"effektiv: {status}", heldTooltip:"effektiv {eff} — selbst schon {own}, wartet auf Abhängigkeiten", a11yOptional:"optional", a11yFocusMark:"hierhin schauen", a11yLink:"verlinkt",
a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: mindestens {size} (angenommen)", a11yTags:"Zuständig: {names}", a11yId:"ID: #{id}", a11yDeps:"hängt ab von: {ids}", a11yFolded:"eingeklappt, {n} verborgen", a11yEffective:"effektiv: {status}", heldTooltip:"effektiv {eff} — selbst schon {own}, wartet auf Abhängigkeiten", a11yOptional:"optional", a11yFocusMark:"hierhin schauen", a11yLink:"verlinkt",
hint_indent:"Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.",
hint_all:"Teilpaket, alle erforderlich", hint_any:"Alternative, eine wählen",
hint_xor:"Alternative, genau eine",
@@ -2251,7 +2251,7 @@ const I18N = {
cheapTooltip:"Highlight the cheapest path unneeded alternatives recede",
leanNextTooltip:"Jump to the next station on the cheapest path what to tackle next ({n} open)",
foldSmallTooltip:"Collapse nodes of size M and smaller press again to expand them all",
implicitSizeTooltip:"No size given assumed as M for the cost estimate",
implicitSizeTooltip:"No size given assumed at least {size} for the cost estimate",
fullscreenTooltip:"Full screen panels use the full window width",
brandTooltip:"“Werkbaum” means roughly work tree — the tree of the work breakdown structure (WBS).",
editorTitle:"Structure (text)", diagramTitle:"Diagram",
@@ -2306,7 +2306,7 @@ const I18N = {
unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.",
sourceLoadWarn:"Could not load “{url}” ({error}). The file must be reachable via http(s) and allow CORS (Access-Control-Allow-Origin).",
sourceTimeoutWarn:"“{url}” did not answer within {seconds} s — the request was aborted. Etherpad limits how often the export may be fetched (10 times per 90 s by default); wait a moment, then reload.",
a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: M (assumed)", a11yTags:"Assigned: {names}", a11yId:"ID: #{id}", a11yDeps:"depends on: {ids}", a11yFolded:"collapsed, {n} hidden", a11yEffective:"effective: {status}", heldTooltip:"effectively {eff} — itself already {own}, waiting on dependencies", a11yOptional:"optional", a11yFocusMark:"look here", a11yLink:"has link",
a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: at least {size} (assumed)", a11yTags:"Assigned: {names}", a11yId:"ID: #{id}", a11yDeps:"depends on: {ids}", a11yFolded:"collapsed, {n} hidden", a11yEffective:"effective: {status}", heldTooltip:"effectively {eff} — itself already {own}, waiting on dependencies", a11yOptional:"optional", a11yFocusMark:"look here", a11yLink:"has link",
hint_indent:"Indentation (2 spaces or a tab) defines the hierarchy.",
hint_all:"sub-task, all required", hint_any:"alternative, choose one",
hint_xor:"alternative, exactly one",
@@ -2355,7 +2355,7 @@ const I18N = {
cheapTooltip:"Resaltar la ruta más económica: las alternativas no necesarias se atenúan",
leanNextTooltip:"Ir a la siguiente estación de la ruta más económica: lo próximo que toca ({n} pendientes)",
foldSmallTooltip:"Plegar los nodos de talla M o menor: pulsa de nuevo para desplegarlos todos",
implicitSizeTooltip:"Sin tamaño indicado: se asume M para el cálculo de costes",
implicitSizeTooltip:"Sin tamaño indicado: se asume al menos {size} para el cálculo de costes",
fullscreenTooltip:"Pantalla completa los paneles usan todo el ancho de la ventana",
brandTooltip:"«Werkbaum» significa algo así como ‘árbol de trabajo — el árbol de la estructura de desglose del trabajo (EDT).",
editorTitle:"Estructura (texto)", diagramTitle:"Diagrama",
@@ -2410,7 +2410,7 @@ const I18N = {
unknownStatusWarn:"Línea {line}: código de estado desconocido «{code}» — mostrado como neutral.",
sourceLoadWarn:"No se pudo cargar «{url}» ({error}). El archivo debe ser accesible por http(s) y permitir CORS (Access-Control-Allow-Origin).",
sourceTimeoutWarn:"«{url}» no respondió en {seconds} s — se canceló la petición. Etherpad limita la frecuencia de descarga del export (10 veces por 90 s de forma predeterminada); espera un momento y vuelve a cargar.",
a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: M (asumido)", a11yTags:"Responsable: {names}", a11yId:"ID: #{id}", a11yDeps:"depende de: {ids}", a11yFolded:"plegado, {n} ocultos", a11yEffective:"efectivo: {status}", heldTooltip:"efectivamente {eff} — por sí mismo ya {own}, espera dependencias", a11yOptional:"opcional", a11yFocusMark:"mirar aquí", a11yLink:"con enlace",
a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: al menos {size} (asumido)", a11yTags:"Responsable: {names}", a11yId:"ID: #{id}", a11yDeps:"depende de: {ids}", a11yFolded:"plegado, {n} ocultos", a11yEffective:"efectivo: {status}", heldTooltip:"efectivamente {eff} — por sí mismo ya {own}, espera dependencias", a11yOptional:"opcional", a11yFocusMark:"mirar aquí", a11yLink:"con enlace",
hint_indent:"La sangría (2 espacios o un tabulador) define la jerarquía.",
hint_all:"subtarea, todas obligatorias", hint_any:"alternativa, elige una",
hint_xor:"alternativa, exactamente una",
@@ -2459,7 +2459,7 @@ const I18N = {
cheapTooltip:"Mettre en évidence le chemin le moins coûteux les alternatives inutiles s'estompent",
leanNextTooltip:"Aller à la station suivante du chemin le moins coûteux la prochaine chose à faire ({n} en attente)",
foldSmallTooltip:"Replier les nœuds de taille M et moins appuyez à nouveau pour tout déplier",
implicitSizeTooltip:"Aucune taille indiquée considérée comme M pour l'estimation des coûts",
implicitSizeTooltip:"Aucune taille indiquée au moins {size} supposé pour l'estimation des coûts",
fullscreenTooltip:"Plein écran les panneaux occupent toute la largeur de la fenêtre",
brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — larbre de lorganigramme des tâches (WBS).",
editorTitle:"Structure (texte)", diagramTitle:"Diagramme",
@@ -2514,7 +2514,7 @@ const I18N = {
unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.",
sourceLoadWarn:"Impossible de charger « {url} » ({error}). Le fichier doit être accessible en http(s) et autoriser CORS (Access-Control-Allow-Origin).",
sourceTimeoutWarn:"« {url} » na pas répondu en {seconds} s — la requête a été interrompue. Etherpad limite la fréquence de récupération de lexport (10 fois par 90 s par défaut) ; attends un instant, puis recharge.",
a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : M (supposé)", a11yTags:"Responsable : {names}", a11yId:"ID : #{id}", a11yDeps:"dépend de : {ids}", a11yFolded:"replié, {n} masqués", a11yEffective:"effectif : {status}", heldTooltip:"effectivement {eff} — lui-même déjà {own}, en attente de dépendances", a11yOptional:"facultatif", a11yFocusMark:"regarder ici", a11yLink:"avec lien",
a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : au moins {size} (supposé)", a11yTags:"Responsable : {names}", a11yId:"ID : #{id}", a11yDeps:"dépend de : {ids}", a11yFolded:"replié, {n} masqués", a11yEffective:"effectif : {status}", heldTooltip:"effectivement {eff} — lui-même déjà {own}, en attente de dépendances", a11yOptional:"facultatif", a11yFocusMark:"regarder ici", a11yLink:"avec lien",
hint_indent:"L'indentation (2 espaces ou une tabulation) définit la hiérarchie.",
hint_all:"sous-tâche, toutes requises", hint_any:"alternative, en choisir une",
hint_xor:"alternative, exactement une",
@@ -2563,7 +2563,7 @@ const I18N = {
cheapTooltip:"Wyróżnij najtańszą ścieżkę niepotrzebne alternatywy są przygaszone",
leanNextTooltip:"Przejdź do następnej stacji najtańszej ścieżki co dalej ({n} otwartych)",
foldSmallTooltip:"Zwiń węzły o rozmiarze M i mniejsze naciśnij ponownie, aby rozwinąć wszystkie",
implicitSizeTooltip:"Nie podano rozmiaru przyjęto M do szacowania kosztów",
implicitSizeTooltip:"Nie podano rozmiaru do szacowania kosztów przyjęto co najmniej {size}",
fullscreenTooltip:"Pełny ekran panele wykorzystują całą szerokość okna",
brandTooltip:"„Werkbaum” znaczy mniej więcej drzewo pracy — drzewo struktury podziału pracy (WBS).",
editorTitle:"Struktura (tekst)", diagramTitle:"Diagram",
@@ -2618,7 +2618,7 @@ const I18N = {
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
sourceLoadWarn:"Nie udało się wczytać „{url}” ({error}). Plik musi być dostępny przez http(s) i zezwalać na CORS (Access-Control-Allow-Origin).",
sourceTimeoutWarn:"„{url}” nie odpowiedział w ciągu {seconds} s — żądanie przerwano. Etherpad ogranicza częstość pobierania eksportu (domyślnie 10 razy na 90 s); odczekaj chwilę i wczytaj ponownie.",
a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: M (założony)", a11yTags:"Przypisano: {names}", a11yId:"ID: #{id}", a11yDeps:"zależy od: {ids}", a11yFolded:"zwinięte, ukrytych: {n}", a11yEffective:"efektywnie: {status}", heldTooltip:"efektywnie {eff} — sam już {own}, czeka na zależności", a11yOptional:"opcjonalny", a11yFocusMark:"spójrz tutaj", a11yLink:"z linkiem",
a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: co najmniej {size} (założony)", a11yTags:"Przypisano: {names}", a11yId:"ID: #{id}", a11yDeps:"zależy od: {ids}", a11yFolded:"zwinięte, ukrytych: {n}", a11yEffective:"efektywnie: {status}", heldTooltip:"efektywnie {eff} — sam już {own}, czeka na zależności", a11yOptional:"opcjonalny", a11yFocusMark:"spójrz tutaj", a11yLink:"z linkiem",
hint_indent:"Wcięcie (2 spacje lub tabulator) definiuje hierarchię.",
hint_all:"podzadanie, wszystkie wymagane", hint_any:"alternatywa, wybierz jedną",
hint_xor:"alternatywa, dokładnie jedna",
@@ -2667,7 +2667,7 @@ const I18N = {
cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются",
leanNextTooltip:"Перейти к следующей станции самого дешёвого пути — что делать дальше ({n} открыто)",
foldSmallTooltip:"Свернуть узлы размера M и меньше — нажмите ещё раз, чтобы развернуть все",
implicitSizeTooltip:"Размер не указан — для оценки затрат принят как M",
implicitSizeTooltip:"Размер не указан — для оценки затрат принят не меньше {size}",
fullscreenTooltip:"Полный экран – панели занимают всю ширину окна",
brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).",
editorTitle:"Структура (текст)", diagramTitle:"Диаграмма",
@@ -2722,7 +2722,7 @@ const I18N = {
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
sourceLoadWarn:"Не удалось загрузить «{url}» ({error}). Файл должен быть доступен по http(s) и разрешать CORS (Access-Control-Allow-Origin).",
sourceTimeoutWarn:"«{url}» не ответил за {seconds} с — запрос прерван. Etherpad ограничивает частоту загрузки экспорта (по умолчанию 10 раз за 90 с); подождите немного и обновите снова.",
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: M (предполагается)", a11yTags:"Ответственные: {names}", a11yId:"ID: #{id}", a11yDeps:"зависит от: {ids}", a11yFolded:"свёрнуто, скрыто: {n}", a11yEffective:"фактически: {status}", heldTooltip:"фактически {eff} — сам уже {own}, ждёт зависимости", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой",
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: не меньше {size} (предполагается)", a11yTags:"Ответственные: {names}", a11yId:"ID: #{id}", a11yDeps:"зависит от: {ids}", a11yFolded:"свёрнуто, скрыто: {n}", a11yEffective:"фактически: {status}", heldTooltip:"фактически {eff} — сам уже {own}, ждёт зависимости", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой",
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
hint_xor:"альтернатива, ровно одна",
@@ -2771,7 +2771,7 @@ const I18N = {
cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं",
leanNextTooltip:"सबसे किफ़ायती पथ के अगले पड़ाव पर जाएँ – अगला काम ({n} शेष)",
foldSmallTooltip:"आकार M और उससे छोटे नोड समेटें – सभी को खोलने के लिए फिर दबाएँ",
implicitSizeTooltip:"कोई आकार नहीं दिया गया – लागत अनुमान के लिए M माना गया",
implicitSizeTooltip:"कोई आकार नहीं दिया गया – लागत अनुमान के लिए कम से कम {size} माना गया",
fullscreenTooltip:"पूर्ण स्क्रीन – पैनल पूरी विंडो चौड़ाई का उपयोग करते हैं",
brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।",
editorTitle:"संरचना (टेक्स्ट)", diagramTitle:"आरेख",
@@ -2826,7 +2826,7 @@ const I18N = {
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
sourceLoadWarn:"„{url}“ लोड नहीं हो सका ({error})। फ़ाइल http(s) से उपलब्ध होनी चाहिए और CORS की अनुमति देनी चाहिए (Access-Control-Allow-Origin)।",
sourceTimeoutWarn:"„{url}“ ने {seconds} स॰ में उत्तर नहीं दिया — अनुरोध रद्द कर दिया गया। Etherpad सीमित करता है कि एक्सपोर्ट कितनी बार लिया जा सके (डिफ़ॉल्ट रूप से 90 स॰ में 10 बार); कुछ क्षण रुकें, फिर दोबारा लोड करें।",
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: M (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yId:"आईडी: #{id}", a11yDeps:"निर्भर: {ids}", a11yFolded:"समेटा हुआ, {n} छिपे", a11yEffective:"प्रभावी: {status}", heldTooltip:"प्रभावी रूप से {eff} — स्वयं {own} है, निर्भरताओं की प्रतीक्षा में", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित",
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: कम से कम {size} (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yId:"आईडी: #{id}", a11yDeps:"निर्भर: {ids}", a11yFolded:"समेटा हुआ, {n} छिपे", a11yEffective:"प्रभावी: {status}", heldTooltip:"प्रभावी रूप से {eff} — स्वयं {own} है, निर्भरताओं की प्रतीक्षा में", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित",
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
hint_xor:"विकल्प, ठीक एक",
@@ -2860,7 +2860,7 @@ const I18N = {
cheapTooltip:"突出显示成本最低的路径——不需要的备选项将淡化",
leanNextTooltip:"跳到成本最低路径的下一站——接下来该做的事(还有 {n} 项)",
foldSmallTooltip:"折叠尺寸 M 及更小的节点——再次点击可全部展开",
implicitSizeTooltip:"未指定尺寸——成本估算时按 M 计",
implicitSizeTooltip:"未指定尺寸——成本估算时按至少 {size} 计",
ghostTooltip:"从 M 号起,元素应进一步细分。",
jumpHint:"Alt+点击:跳转到文本中的该行",
jumpHintTouch:"长按:跳转到文本中的该行",
@@ -2930,7 +2930,7 @@ const I18N = {
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
sourceLoadWarn:"无法加载“{url}”({error})。该文件必须可通过 http(s) 访问并允许 CORSAccess-Control-Allow-Origin)。",
sourceTimeoutWarn:"“{url}” 在 {seconds} 秒内没有响应 — 请求已中止。Etherpad 会限制导出的获取频率(默认每 90 秒 10 次);请稍候再重新加载。",
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:M(假定)", a11yTags:"负责人:{names}", a11yId:"ID#{id}", a11yDeps:"依赖:{ids}", a11yFolded:"已折叠,隐藏 {n} 项", a11yEffective:"实际:{status}", heldTooltip:"实际为 {eff}——自身已是 {own},等待依赖完成", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接",
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:至少 {size}(假定)", a11yTags:"负责人:{names}", a11yId:"ID#{id}", a11yDeps:"依赖:{ids}", a11yFolded:"已折叠,隐藏 {n} 项", a11yEffective:"实际:{status}", heldTooltip:"实际为 {eff}——自身已是 {own},等待依赖完成", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接",
hint_indent:"缩进(2 个空格或制表符)定义层级。",
hint_all:"子任务,全部必需", hint_any:"备选项,择其一",
hint_xor:"备选项,恰好一个",
@@ -2964,7 +2964,7 @@ const I18N = {
cheapTooltip:"最も低コストの経路を強調 – 不要な選択肢は控えめに表示",
leanNextTooltip:"最も低コストの経路の次の駅へ移動 – 次にやること(残り {n} 件)",
foldSmallTooltip:"サイズ M 以下のノードを折りたたむ – もう一度押すとすべて展開",
implicitSizeTooltip:"サイズ未指定 – コスト見積もりのため M として扱う",
implicitSizeTooltip:"サイズ未指定 – コスト見積もりのため少なくとも {size} として扱う",
ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。",
jumpHint:"Alt+クリック:テキストの該当行へ移動",
jumpHintTouch:"長押し:テキストの該当行へ移動",
@@ -3034,7 +3034,7 @@ const I18N = {
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
sourceLoadWarn:"「{url}」を読み込めませんでした({error})。ファイルは http(s) でアクセス可能で、CORSAccess-Control-Allow-Origin)を許可する必要があります。",
sourceTimeoutWarn:"「{url}」が {seconds} 秒以内に応答しませんでした — 要求を中止しました。Etherpad はエクスポートの取得回数を制限します(既定で 90 秒あたり 10 回)。少し待ってから再読み込みしてください。",
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: M(想定)", a11yTags:"担当: {names}", a11yId:"ID: #{id}", a11yDeps:"依存先: {ids}", a11yFolded:"折りたたみ中、{n} 件非表示", a11yEffective:"実効: {status}", heldTooltip:"実効では {eff} — 自身は既に {own}、依存待ち", a11yOptional:"任意", a11yFocusMark:"ここを見る", a11yLink:"リンクあり",
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: 少なくとも {size}(想定)", a11yTags:"担当: {names}", a11yId:"ID: #{id}", a11yDeps:"依存先: {ids}", a11yFolded:"折りたたみ中、{n} 件非表示", a11yEffective:"実効: {status}", heldTooltip:"実効では {eff} — 自身は既に {own}、依存待ち", a11yOptional:"任意", a11yFocusMark:"ここを見る", a11yLink:"リンクあり",
hint_indent:"インデント(スペース2つまたはタブ)で階層を定義します。",
hint_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ",
hint_xor:"選択肢、ちょうど1つ",
+42 -6
View File
@@ -24,9 +24,9 @@ export function needsBreakdown(n){
/* Faltung-Voreinstellung Größe M und kleiner zuklappen" (SPEC §9, D44):
Knoten mit ANGEGEBENER Größe bis einschließlich M offen bleiben also nur
L, XL, XXL. Fehlt die Angabe, wird nichts angenommen: Der günstigste Pfad
rechnet sie zwar als M (D18), das ist aber eine Kostenannahme des Werkzeugs
und keine Aussage des Autors; danach den Baum zuzuklappen hieße, eine
Vermutung wie eine Angabe zu behandeln. */
schätzt sie zwar (`assumedSize`, D18/D66), das ist aber eine Kostenannahme
des Werkzeugs und keine Aussage des Autors; danach den Baum zuzuklappen
hieße, eine Vermutung wie eine Angabe zu behandeln. */
export function atMostM(n){ return !!n.size && SIZE_RANK[n.size] <= SIZE_RANK.M; }
/* Sichtbare Kinder: verworfene ausblenden, außer showDiscarded ist gesetzt. */
@@ -40,7 +40,8 @@ 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. Ist in einer disjunktiven Gruppe etwas realisiert, wird nur noch
Größe wird aus den Teilpaketen geschätzt (`assumedSize`, D66).
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 alle
@@ -80,8 +81,43 @@ 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; }
/* ---------- Geschätzte Größe bei fehlender Angabe (SPEC §9, D66) ----------
Statt pauschal M (die alte D18-Annahme) wird aus den Teilpaketen geschätzt:
MINDESTENS die größte Größe der zählenden Kinder; tragen drei oder mehr
Kinder diese größte Größe, eine Stufe mehr (Deckel XXL). Es zählen dieselben
Kinder wie beim Größen-Konflikt (§5/D62) direkte, verworfene und optionale
nie, disjunktiv (`|`/`=`) nur die kleinste Alternative (aus `chosenPool`,
D61: eine getroffene Wahl gilt) nur dass Kinder OHNE Größe hier rekursiv
mitgeschätzt werden: geschätzt wird ohnehin. Ohne zählende Kinder bleibt es
beim M-Rückfall. Memo per WeakMap: `computeCheapPlan` ruft `ownCost` je
Suchbelegung über die ganze Menge ungecacht wäre das O() je Belegung;
der Parse-Baum wird bei jedem Tastendruck neu gebaut, der Cache kann also
nie veralten. */
const SIZE_BY_RANK = ['XS', 'S', 'M', 'L', 'XL', 'XXL'];
const ASSUMED = new WeakMap();
export function assumedSize(n){
if(n.size) return n.size;
const memo = ASSUMED.get(n);
if(memo) return memo;
const kids = n.children.filter(k =>
!k.optional && (!k.status || k.status.key !== 'verworfen'));
let size = 'M';
if(kids.length){
if(gateOf(kids) !== 'and'){
/* nur eine Alternative wird realisiert — die kleinste ist der Boden */
size = SIZE_BY_RANK[Math.min(...chosenPool(kids).map(k => SIZE_RANK[assumedSize(k)]))];
} else {
const ranks = kids.map(k => SIZE_RANK[assumedSize(k)]);
const max = Math.max(...ranks);
const atMax = ranks.filter(r => r === max).length;
size = SIZE_BY_RANK[Math.min(max + (atMax >= 3 ? 1 : 0), SIZE_RANK.XXL)];
}
}
ASSUMED.set(n, size);
return size;
}
/* 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 cheapestCost(n){
const kids = pathChildren(n);
let c = ownCost(n);
+14 -11
View File
@@ -7,7 +7,7 @@
opts = {
t, // i18n-Funktion (key, vars?) -> String
showDiscarded, // verworfene einblenden?
cheapPath, // günstigster Pfad aktiv? (steuert das implizite M-Badge)
cheapPath, // günstigster Pfad aktiv? (steuert das implizite Größen-Badge)
cheapSet, // Set der nötigen Knoten (leer, wenn Pfad aus)
freshSet, // optional: Knoten, die neu in Produktion sind (D28)
collapsedSet, // optional: eingeklappte Knoten (Faltung, SPEC §9/D38)
@@ -15,7 +15,7 @@
// Diskrepanzen (effectiveStatus() in model.js, D39)
} */
import { gateOf, needsBreakdown, visibleChildren, cheapCls, isDone } from './model.js';
import { gateOf, needsBreakdown, visibleChildren, cheapCls, isDone, assumedSize } from './model.js';
/* Zusatzklassen eines Knotens: günstigster Pfad (D18), neu in Produktion"
gegenüber der zuletzt gesehenen Fassung (D28, `freshSet` optional) und
@@ -106,7 +106,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 && !isDone(n)) parts.push(t('a11ySizeImplicit'));
else if(cheapPath && !isDone(n)) parts.push(t('a11ySizeImplicit', {size: assumedSize(n)}));
/* Größen-Konflikt (SPEC §5/D62): die Warnfärbung des Badges kommt beim
Screenreader sonst nicht an. */
if(n.sizeConflict) parts.push(t('sizeConflictTooltip'));
@@ -172,10 +172,12 @@ function nodeHtml(n, extra, opts, fold){
? t('heldTooltip', {eff: t('st_' + effKey), own: t('st_' + n.status.key)})
: (n.status ? t('st_' + n.status.key) : ''),
n.optional ? t('a11yOptional') : '',
/* Die Kostenannahme Größe fehlt, gilt als M" (D18) hatte einen
eigenen `title` am Badge der erschiene neben dem Fenster
ein zweites Mal. Sie gehört ohnehin zu den Kurz-Fakten. */
(!n.size && cheapPath && !isDone(n)) ? t('implicitSizeTooltip') : '',
/* Die Kostenannahme Größe fehlt, mindestens angenommen"
(D18/D66) hatte einen eigenen `title` am Badge der
erschiene neben dem Fenster ein zweites Mal. Sie gehört
ohnehin zu den Kurz-Fakten. */
(!n.size && cheapPath && !isDone(n))
? t('implicitSizeTooltip', {size: assumedSize(n)}) : '',
/* Größen-Konflikt (SPEC §5/D62): das warnfarbene Badge braucht
seine Begründung dort, wo man nachsieht. */
n.sizeConflict ? t('sizeConflictTooltip') : '',
@@ -191,15 +193,16 @@ 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. */
/* Das implizite Größen-Badge macht eine KOSTENANNAHME sichtbar (D18)
seit D66 die aus den Teilpaketen geschätzte Größe statt pauschal M. An
einem erledigten Knoten wird keine getroffen er kostet nichts mehr
(D46) , dort bleibt es deshalb weg. */
const implicitSize = !n.size && cheapPath && !isDone(n);
/* Größen-Konflikt (SPEC §5/D62): das Badge wechselt auf die Warnfarbe
die Größe selbst bleibt stehen, korrigiert wird nichts. */
const sizeBadge = n.size
? `<span class="size${n.sizeConflict ? ' conflict' : ''}" aria-hidden="true">${n.size}</span>`
: (implicitSize ? `<span class="size implicit" aria-hidden="true">M</span>` : '');
: (implicitSize ? `<span class="size implicit" aria-hidden="true">${assumedSize(n)}</span>` : '');
/* High-Risk: Warndreieck (, Textpräsentation via VS15) an der oberen linken
Ecke. aria-hidden die Information steckt bereits im Status des aria-label. */
/* Kein eigener `title` mehr: Er zeigte sonst zusätzlich zum Knoten-Fenster
+158
View File
@@ -0,0 +1,158 @@
import { describe, it, expect } from 'vitest';
import { parse } from '../src/parser.js';
import { assumedSize, ownCost, cheapestCost } from '../src/model.js';
import { renderTreeHtml } from '../src/render.js';
/* Geschätzte Größe bei fehlender Angabe (SPEC §9, D66): statt pauschal M wird
aus den Teilpaketen geschätzt mindestens die größte Größe der zählenden
Kinder, ab drei Kindern dieser Größe eine Stufe mehr (Deckel XXL). Es zählen
dieselben Kinder wie beim Größen-Konflikt (§5/D62), nur dass fehlende
Kindgrößen rekursiv mitgeschätzt werden. */
const roots = txt => parse(txt).roots;
const t = (key, vars) => {
let s = key;
if(vars) for(const k in vars) s += ':' + vars[k];
return s;
};
const opts = () => ({ t, showDiscarded: false, cheapPath: true, cheapSet: new Set() });
describe('assumedSize — Schätzung aus den Teilpaketen (D66)', () => {
it('Blatt ohne Größe bleibt M (der alte D18-Rückfall)', () => {
expect(assumedSize(roots('[ ] Blatt')[0])).toBe('M');
});
it('explizite Größe gewinnt immer', () => {
const [n] = roots(`[ ] Eltern (S)
- [ ] Kind (XL)`);
expect(assumedSize(n)).toBe('S');
});
it('mindestens die größte Kindgröße', () => {
const [n] = roots(`[ ] Eltern
- [ ] A (L)
- [ ] B (S)`);
expect(assumedSize(n)).toBe('L');
});
it('zwei Kinder der größten Größe: noch keine Stufe mehr', () => {
const [n] = roots(`[ ] Eltern
- [ ] A (S)
- [ ] B (S)`);
expect(assumedSize(n)).toBe('S');
});
it('drei Kinder der größten Größe: eine Stufe mehr', () => {
const [n] = roots(`[ ] Eltern
- [ ] A (S)
- [ ] B (S)
- [ ] C (S)
- [ ] D (XS)`);
expect(assumedSize(n)).toBe('M');
});
it('Deckel XXL: drei XXL-Kinder bleiben XXL', () => {
const [n] = roots(`[ ] Eltern
- [ ] A (XXL)
- [ ] B (XXL)
- [ ] C (XXL)`);
expect(assumedSize(n)).toBe('XXL');
});
it('Kinder ohne Größe werden rekursiv mitgeschätzt', () => {
/* Das Kind hat selbst 3×M-Kinder ⇒ geschätzt L; der Elternknoten erbt L. */
const [n] = roots(`[ ] Eltern
- [ ] Kind
- [ ] a (M)
- [ ] b (M)
- [ ] c (M)`);
expect(assumedSize(n.children[0])).toBe('L');
expect(assumedSize(n)).toBe('L');
});
it('drei größenlose Blätter zählen als 3×M ⇒ L', () => {
const [n] = roots(`[ ] Eltern
- [ ] a
- [ ] b
- [ ] c`);
expect(assumedSize(n)).toBe('L');
});
it('optionale und verworfene Kinder zählen nicht', () => {
const [n] = roots(`[ ] Eltern
- [ ] A (S)
+ [ ] Zugabe (XXL)
- [-] Weg (XXL)`);
expect(assumedSize(n)).toBe('S');
});
it('nur optionale Kinder ⇒ M-Rückfall', () => {
const [n] = roots(`[ ] Eltern
+ [ ] Zugabe (XL)`);
expect(assumedSize(n)).toBe('M');
});
it('disjunktive Gruppe: die kleinste Alternative ist der Boden', () => {
const [n] = roots(`[ ] Eltern
| [ ] A (XL)
| [ ] B (S)`);
expect(assumedSize(n)).toBe('S');
});
it('disjunktiv mit getroffener Wahl (D61): die kleinste der realisierten', () => {
const [n] = roots(`[ ] Eltern
= [~] A (XL)
= [ ] B (S)`);
expect(assumedSize(n)).toBe('XL');
});
it('drei gleich große Alternativen bleiben ohne Stufe mehr — nur eine wird realisiert', () => {
const [n] = roots(`[ ] Eltern
| [ ] A (L)
| [ ] B (L)
| [ ] C (L)`);
expect(assumedSize(n)).toBe('L');
});
});
describe('ownCost und Pfad rechnen mit der Schätzung', () => {
it('ownCost eines größenlosen Elternknotens folgt der Schätzung', () => {
const [n] = roots(`[ ] Eltern
- [ ] Kind (XL)`);
expect(ownCost(n)).toBe(ownCost(roots('[ ] X (XL)')[0]));
});
it('cheapestCost steigt gegenüber der alten M-Annahme', () => {
const derived = cheapestCost(roots(`[ ] Eltern
- [ ] Kind (XL)`)[0]);
const stated = cheapestCost(roots(`[ ] Eltern (M)
- [ ] Kind (XL)`)[0]);
expect(derived).toBeGreaterThan(stated);
});
it('am erledigten Knoten wird weiterhin nichts angenommen (Kosten 0)', () => {
const [n] = roots(`[x] Eltern
- [x] Kind (XL)`);
expect(ownCost(n)).toBe(0);
});
});
describe('Badge zeigt die geschätzte Größe', () => {
it('das invertierte Badge trägt die abgeleitete Größe statt M', () => {
const { html } = renderTreeHtml(roots(`[ ] Eltern
- [ ] Kind (XL)`), opts());
expect(html).toContain('<span class="size implicit" aria-hidden="true">XL</span>');
});
it('Tooltip und aria-label nennen die geschätzte Größe', () => {
const { html } = renderTreeHtml(roots(`[ ] Eltern
- [ ] Kind (L)`), opts());
expect(html).toContain('implicitSizeTooltip:L');
expect(html).toContain('a11ySizeImplicit:L');
});
it('Blatt ohne Größe zeigt weiterhin M', () => {
const { html } = renderTreeHtml(roots('[ ] Blatt'), opts());
expect(html).toContain('<span class="size implicit" aria-hidden="true">M</span>');
});
});