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
+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(n²) 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>');
});
});