feat: Zuständigen-Engpass — Warnung, wenn eine Person mehr als die Hälfte des Pfads trägt (D71)
Trägt ein @name mehr als die Hälfte der offenen Arbeit des günstigsten Pfads (Marginalkosten-Maß, D69) und tragen mindestens zwei Personen Last auf der offenen Front, meldet die zeilenlose Warnung assigneeOverload die mögliche Engstelle; die Pillen der Person wechseln an offenen Pfad-Knoten auf die Warnfarbe. Knoten ohne Tags erben für die Rechnung vom nächsten getaggten Vorfahren, mehrere Tags teilen sich den Beitrag. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d1da6a3529
commit
35b72fc178
@@ -19,6 +19,7 @@ reverse.
|
||||
|
||||
## 2026-08-25
|
||||
|
||||
- A warning flags the bottleneck when one `@name` carries more than half of the open work on the cheapest path — their tag pills turn amber on the open nodes
|
||||
- The estimated size of an unsized node now prices the remaining work — done sub-packages drop out, and a subtree with everything named done counts as XS
|
||||
- The fold chip is solid white with an ink glyph now — the translucent box vanished on the pastel node colours
|
||||
- A size prices its whole subtree on the cheapest path — sub-packages no longer add on top, so a carefully decomposed `(S)` beats a coarse `(L)`
|
||||
|
||||
@@ -5219,3 +5219,72 @@ fallen; XS-Boden auf M zurück → genau die drei XS-Tests; „Gruppe fertig"
|
||||
entfernt → genau die zwei disjunktiven. Der mitgelieferte Plan ist
|
||||
unberührt (sein einziger größenloser Knoten ist ein Blatt): weiterhin
|
||||
0 Warnungen, 137 Pfadknoten, 57 Stationen, exakt gerechnet.
|
||||
|
||||
## D71 — Zuständigen-Engpass: warnen, wenn eine Person mehr als die Hälfte des Pfads trägt
|
||||
Gewünscht: „eine Warnung, wenn ein und derselbe Verantwortliche (@name) zu oft
|
||||
auf dem Lean-Path liegt — so eine Art Konflikt-Indikator." Kein neues
|
||||
Zeichen — Tags (§7) und der günstigste Pfad existieren, es fehlte der
|
||||
Konsument, der sie übereinanderlegt. Die Entscheidungen (Vorschlag bestätigt):
|
||||
|
||||
**Was der Indikator sagt:** Werkbaum ist bewusst kein Netzplan — die Warnung
|
||||
behauptet nichts über Termine, sondern: Die **nächste Ausbaustufe ist nicht
|
||||
parallelisierbar**, alles Weitere serialisiert sich durch eine Person. Genau
|
||||
die Auskunft, die man beim Lean Pathfinding braucht.
|
||||
|
||||
**Maß: Marginalkosten, Stationen nur für den Text.** Zählen und Gewichten
|
||||
standen zur Wahl; gewählt ist die Rollenteilung — Schwelle und Anteil rechnen
|
||||
**gewichtet** (5×XS sind weniger Last als 2×XL), der Meldungstext nennt
|
||||
zusätzlich die **Stationszahl**, weil Stationen sichtbare, mit dem Auge
|
||||
nachprüfbare Objekte sind (die Punkte, die der D47-Knopf abgeht). Das
|
||||
Gewichts-Maß gab es schon: die Marginalkosten des Belegungs-Vergleichs
|
||||
(D69) — jeder nötige Knoten zählt mit dem, was seine Größe über die nötigen
|
||||
Teilpakete hinaus behauptet; Erledigtes hat `ownCost` 0 und fällt von selbst
|
||||
heraus. Die Personen-Summen ergeben zusammen exakt den Pfadpreis — kein
|
||||
zweites Kostenmodell.
|
||||
|
||||
**Zuständigkeit erbt vom nächsten getaggten Vorfahren.** Die übliche
|
||||
WBS-Lesart: Wer das Paket hat, hat die Teilpakete. Ohne Vererbung zählte in
|
||||
sparsam getaggten Plänen fast nichts — im Test trüge die Person eines
|
||||
zerlegten L-Pakets nur dessen Marge (1 statt 4). Mehrere Tags einer Zeile
|
||||
**teilen** sich den Beitrag zu gleichen Teilen — voll doppelt gezählt bliese
|
||||
gemeinsame Pakete künstlich zum Engpass auf. Beiträge ohne getaggten
|
||||
Vorfahren gehen nur in die Gesamtsumme ein: Sie **verwässern** die Anteile
|
||||
(konservativ — gemeldet wird nur, was sicher ist), warnen aber nicht selbst;
|
||||
„viel ist niemandem zugewiesen" wäre eine eigene Aussage und, wenn überhaupt,
|
||||
eine eigene Meldung.
|
||||
|
||||
**Schwelle: strikt mehr als die Hälfte, und mindestens zwei Personen mit Last
|
||||
auf der offenen Front.** Der Solo-Plan ist die Falle — eine Person mit 100 %
|
||||
ist dort keine Engstelle, sondern die Realität. Gezählt werden Personen **auf
|
||||
der Front** (Last > 0), nicht im Dokument: Wer nur erledigte Knoten trägt,
|
||||
macht aus einem faktischen Solo-Rest kein Zwei-Personen-Problem. Mehr als
|
||||
eine Person über der Hälfte kann es nicht geben — die Meldung ist eindeutig.
|
||||
Die Schwelle ist gesetzt, nicht hergeleitet (wie die 32 Zeichen in D64);
|
||||
justiert wird nach Erfahrung.
|
||||
|
||||
**Anzeige: zeilenlose Warnung plus Warn-Pille.** Der Engpass hat keine
|
||||
einzelne Zeile — `assigneeOverload` ist zeilenlos wie `cheapApprox` und nennt
|
||||
Person, Anteil und Stationen. Zusätzlich wechseln die **Personen-Pillen** der
|
||||
betroffenen Person an offenen Pfad-Knoten auf `--warn` — derselbe Griff wie
|
||||
beim Größen-Konflikt-Badge (D62); der Grafikexport folgt von selbst, weil er
|
||||
die gemessenen Pillen-Farben liest (D62). Geerbte Zuständigkeit hat keine
|
||||
Pille und damit keine Färbung — benannt, kein Fehler. Kein neuer Umschalter,
|
||||
kein Panel; bei ausgeschaltetem Pfad entfällt beides (ohne Pfad keine offene
|
||||
Front).
|
||||
|
||||
**Headless nach Hausregel** (D54-Nachtrag 3): `assigneeLoads`/
|
||||
`overloadedAssignee` in model.js, der Renderer bekommt nur `overloadTag`;
|
||||
app.js verdrahtet Warnung und Option. Die mitgelieferten Dokumente bleiben
|
||||
still (der Werkbaum-Plan hat keine Knoten-Tags; im Example liegt anna bei
|
||||
3 von 13) — nachgemessen, nicht angenommen.
|
||||
|
||||
**Nachgemessen** im Browser an einem Wegwerf-Dokument: „@anna trägt 80 % der
|
||||
offenen Arbeit auf dem günstigsten Pfad (1 von 2 Stationen) — mögliche
|
||||
Engstelle.", genau eine Pille bernstein (`rgb(180,83,9)`) — am offenen
|
||||
Pfad-Knoten, nicht am erledigten und nicht bei @ben; Pfad aus ⇒ Warnung und
|
||||
Färbung weg, wieder an ⇒ beides zurück. 470 Tests, davon 13 neue in
|
||||
`tests/overload.test.js`. Gegenproben per Mutation: Solo-Wächter entfernt →
|
||||
genau der Solo-Test fällt; Schwelle aufgeweicht → genau die drei
|
||||
Schwellen-Tests; Vererbung entfernt → genau der Vererbungs-Test; Teilung
|
||||
entfernt → genau der Mehrfach-Tag-Test; Erledigt-Ausnahme der Pille
|
||||
entfernt → genau der Pillen-Test.
|
||||
|
||||
@@ -396,6 +396,9 @@ Größe als **Bereich** gelesen (Untergrenzen verdoppeln sich):
|
||||
- `@name` mit `name` aus Unicode-Buchstaben, Ziffern, `.`, `_`, `-`.
|
||||
- Mehrere Tags pro Zeile möglich, Position im Text egal.
|
||||
- Anzeige: helle Pillen unten rechts an der Knoten-Ecke.
|
||||
- Für den **Zuständigen-Engpass** (§9) erbt ein Knoten ohne Tags die Tags des
|
||||
nächsten Vorfahren mit Tags — nur für diese Rechnung; an der Anzeige der
|
||||
Pillen ändert das nichts.
|
||||
|
||||
## 8. Kommentare
|
||||
|
||||
@@ -727,6 +730,34 @@ vorn. Der Knopf ist **verborgen**, solange es keine Station gibt — bei
|
||||
ausgeschaltetem Pfad ebenso wie bei einem durchweg erledigten Plan; sein
|
||||
Tooltip nennt die Zahl der offenen Stationen. Siehe D47.
|
||||
|
||||
**Zuständigen-Engpass.** Trägt **eine** Person zu viel der offenen Arbeit des
|
||||
günstigsten Pfads, meldet eine zeilenlose Warnung `assigneeOverload` die
|
||||
mögliche Engstelle: Die nächste Ausbaustufe ist dann nicht parallelisierbar —
|
||||
alles Weitere serialisiert sich durch diese Person. Über Reihenfolge oder
|
||||
Termine sagt die Warnung nichts (das tut in dieser Notation ohnehin nichts,
|
||||
§1). Siehe D71.
|
||||
|
||||
- **Last:** Jeder nötige Knoten zählt mit dem, was seine Größe über die
|
||||
nötigen Teilpakete hinaus behauptet (dasselbe Maß wie der Belegungs-Vergleich
|
||||
der Closure-Suche, oben) — Erledigtes also mit 0. Mehrere `@name`-Tags einer
|
||||
Zeile teilen sich den Beitrag zu gleichen Teilen.
|
||||
- **Zuständig** ist, wer auf der Zeile getaggt ist (§7); ein Knoten **ohne**
|
||||
Tags erbt für diese Rechnung die Tags des nächsten Vorfahren **mit** Tags —
|
||||
die übliche WBS-Lesart: Wer das Paket hat, hat die Teilpakete. Ohne
|
||||
getaggten Vorfahren zählt der Beitrag als nicht zugewiesen — er verwässert
|
||||
die Anteile, warnt aber nicht selbst.
|
||||
- **Gemeldet** wird, wenn mindestens **zwei** Personen Last auf der offenen
|
||||
Front tragen und eine davon **mehr als die Hälfte** der gesamten offenen
|
||||
Pfad-Arbeit hält. Ein Solo-Plan warnt damit nie — eine Person, die alles
|
||||
trägt, ist dort keine Engstelle, sondern die Realität. Die Schwelle ist
|
||||
gesetzt, nicht hergeleitet (wie die ~32 Zeichen des Label-Umbruchs).
|
||||
- **Anzeige:** Die Warnung nennt Person, Anteil und Stationen („@anna trägt
|
||||
62 % der offenen Arbeit … 7 von 9 Stationen"); zusätzlich wechseln die
|
||||
Personen-Pillen (§7) der betroffenen Person an **offenen Pfad-Knoten** auf
|
||||
die Warnfarbe (`--warn`) — auch im Grafikexport. Geerbte Zuständigkeit hat
|
||||
keine Pille und damit keine Färbung. Bei ausgeschaltetem Pfad entfällt
|
||||
beides — ohne Pfad gibt es keine offene Front.
|
||||
|
||||
### Diagramm aus einer URL laden (`?sourceUrl=`)
|
||||
Der Editor kann den Notationstext aus einer externen Textdatei beziehen:
|
||||
`…?sourceUrl=https://example.org/plan.txt` (relative Angaben werden gegen die
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
- [^] #ed.path.started: Started work is on the path, extras included (S) %% see D61
|
||||
- [^] #ed.path.line: Metro-map line through the leaves (S)
|
||||
- [^] #ed.path.step: Step from station to station, one button (S) %% see D47
|
||||
- [x] #ed.path.load: Warn when one person is the bottleneck (S) %% see D71
|
||||
- [^] #ed.closure: Count the whole dependency closure (M)
|
||||
- [^] #ed.closure.once: Shared dependencies counted only once (S)
|
||||
- [^] #ed.closure.nonlocal: The cheapest alternative is no longer the local one (M) %% see D34, D42
|
||||
@@ -423,6 +424,12 @@
|
||||
next. Seeing the front was solved; walking it across a very wide tree was
|
||||
not.
|
||||
|
||||
#ed.path.load
|
||||
Warns when one @name carries more than half of the open work on the
|
||||
cheapest path while at least two people are on it: the next increment
|
||||
would serialize through that person. Untagged nodes inherit the nearest
|
||||
tagged ancestor, and the person's tag pills turn amber on open path nodes.
|
||||
|
||||
#ed.closure
|
||||
The cheapest path pulls every dependency of a needed node into the needed
|
||||
set, and shared targets count only once. That is why an alternative can win
|
||||
|
||||
+18
-3
@@ -1,6 +1,6 @@
|
||||
import './style.css';
|
||||
import { parse, setFoldMark, expandShortIds, shortIdClosed } from './parser.js';
|
||||
import { computeCheapPlan, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, atMostM, lineTargets } from './model.js';
|
||||
import { computeCheapPlan, overloadedAssignee, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, atMostM, lineTargets } from './model.js';
|
||||
import { esc, renderTreeHtml, TIP_RULE } from './render.js';
|
||||
import { formatWarning, warningText } from './warnings.js';
|
||||
import { padUrls } from './remote.js';
|
||||
@@ -133,11 +133,16 @@ function render(){
|
||||
/* Günstigster Pfad auf der Dependency Closure (D42): scheitert die exakte
|
||||
Suche an zu vielen gekoppelten Gruppen, wird die gierige Schätzung
|
||||
BENANNT statt stillschweigend geliefert (zeilenlose Warnung). */
|
||||
let cheapSet = new Set();
|
||||
let cheapSet = new Set(), overload = null;
|
||||
if(cheapPathOn){
|
||||
const plan = computeCheapPlan(roots);
|
||||
cheapSet = plan.set;
|
||||
if(!plan.exact) warnings = warnings.concat([{type: 'cheapApprox'}]);
|
||||
/* Zuständigen-Engpass (SPEC §9/D71): Trägt eine Person mehr als die
|
||||
Hälfte der offenen Pfad-Arbeit, wird das zeilenlos gemeldet und ihre
|
||||
Pillen an offenen Pfad-Knoten warnfarben (overloadTag unten). */
|
||||
overload = overloadedAssignee(roots, cheapSet);
|
||||
if(overload) warnings = warnings.concat([{type: 'assigneeOverload', ...overload}]);
|
||||
}
|
||||
out.classList.toggle('cheap-on', cheapPathOn);
|
||||
/* Faltung (D38): Anfangszustand aus den Textmarken (`!!!` holt sich mit
|
||||
@@ -185,7 +190,8 @@ function render(){
|
||||
lineTargetMap = lineTargets(roots, collapsedSet, showDiscarded);
|
||||
const r = renderTreeHtml(roots, {t, showDiscarded, cheapPath: cheapPathOn, cheapSet, showIds,
|
||||
freshSet, collapsedSet,
|
||||
effStatus: effectiveStatus(roots)});
|
||||
effStatus: effectiveStatus(roots),
|
||||
overloadTag: overload ? overload.tag : null});
|
||||
out.innerHTML = r.html;
|
||||
warnings = warnings.concat(r.warnings);
|
||||
}
|
||||
@@ -2220,6 +2226,7 @@ const I18N = {
|
||||
sizeConflictWarn:"Zeile {line}: Die Teilpakete übersteigen zusammen die angegebene Größe ({size}) — selbst in der günstigsten Lesart.",
|
||||
sizeConflictTooltip:"Die Teilpakete übersteigen zusammen die angegebene Größe",
|
||||
cheapApproxWarn:"Zu viele gekoppelte Alternativgruppen für die exakte Suche — der günstigste Pfad ist gierig geschätzt (je Gruppe lokal gewählt).",
|
||||
assigneeOverloadWarn:"@{tag} trägt {share} % der offenen Arbeit auf dem günstigsten Pfad ({stations} von {total} Stationen) — mögliche Engstelle.",
|
||||
st_idee:"Idee", st_geplant:"geplant", st_arbeit:"in Arbeit", st_durchstich:"Durchstich",
|
||||
st_fertig:"fertig", st_prod:"in Produktion", st_highrisk:"High Risk", st_verworfen:"verworfen",
|
||||
unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.",
|
||||
@@ -2324,6 +2331,7 @@ const I18N = {
|
||||
sizeConflictWarn:"Line {line}: the sub-packages together exceed the given size ({size}) — even in the most optimistic reading.",
|
||||
sizeConflictTooltip:"The sub-packages together exceed the given size",
|
||||
cheapApproxWarn:"Too many coupled alternative groups for the exact search — the cheapest path is a greedy estimate (chosen locally per group).",
|
||||
assigneeOverloadWarn:"@{tag} carries {share}% of the open work on the cheapest path ({stations} of {total} stations) — a possible bottleneck.",
|
||||
st_idee:"idea", st_geplant:"planned", st_arbeit:"in progress", st_durchstich:"walking skeleton",
|
||||
st_fertig:"done", st_prod:"in production", st_highrisk:"high risk", st_verworfen:"discarded",
|
||||
unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.",
|
||||
@@ -2428,6 +2436,7 @@ const I18N = {
|
||||
sizeConflictWarn:"Línea {line}: los subpaquetes juntos superan el tamaño indicado ({size}), incluso en la lectura más optimista.",
|
||||
sizeConflictTooltip:"Los subpaquetes juntos superan el tamaño indicado",
|
||||
cheapApproxWarn:"Demasiados grupos de alternativas acoplados para la búsqueda exacta — el camino más barato es una estimación voraz (elección local por grupo).",
|
||||
assigneeOverloadWarn:"@{tag} lleva el {share} % del trabajo pendiente en el camino más barato ({stations} de {total} estaciones) — posible cuello de botella.",
|
||||
st_idee:"idea", st_geplant:"planificado", st_arbeit:"en curso", st_durchstich:"prototipo funcional",
|
||||
st_fertig:"terminado", st_prod:"en producción", st_highrisk:"alto riesgo", st_verworfen:"descartado",
|
||||
unknownStatusWarn:"Línea {line}: código de estado desconocido «{code}» — mostrado como neutral.",
|
||||
@@ -2532,6 +2541,7 @@ const I18N = {
|
||||
sizeConflictWarn:"Ligne {line} : les sous-lots dépassent ensemble la taille indiquée ({size}), même dans la lecture la plus optimiste.",
|
||||
sizeConflictTooltip:"Les sous-lots dépassent ensemble la taille indiquée",
|
||||
cheapApproxWarn:"Trop de groupes d’alternatives couplés pour la recherche exacte — le chemin le moins cher est une estimation gloutonne (choix local par groupe).",
|
||||
assigneeOverloadWarn:"@{tag} porte {share} % du travail restant sur le chemin le moins cher ({stations} stations sur {total}) — goulot d’étranglement possible.",
|
||||
st_idee:"idée", st_geplant:"planifié", st_arbeit:"en cours", st_durchstich:"squelette fonctionnel",
|
||||
st_fertig:"terminé", st_prod:"en production", st_highrisk:"risque élevé", st_verworfen:"abandonné",
|
||||
unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.",
|
||||
@@ -2636,6 +2646,7 @@ const I18N = {
|
||||
sizeConflictWarn:"Wiersz {line}: podzadania razem przekraczają podany rozmiar ({size}) — nawet w najkorzystniejszym odczycie.",
|
||||
sizeConflictTooltip:"Podzadania razem przekraczają podany rozmiar",
|
||||
cheapApproxWarn:"Zbyt wiele sprzężonych grup alternatyw dla dokładnego wyszukiwania — najtańsza ścieżka jest oszacowana zachłannie (wybór lokalny w każdej grupie).",
|
||||
assigneeOverloadWarn:"@{tag} niesie {share} % otwartej pracy na najtańszej ścieżce ({stations} z {total} stacji) — możliwe wąskie gardło.",
|
||||
st_idee:"pomysł", st_geplant:"zaplanowane", st_arbeit:"w toku", st_durchstich:"działający szkielet",
|
||||
st_fertig:"gotowe", st_prod:"w produkcji", st_highrisk:"wysokie ryzyko", st_verworfen:"odrzucone",
|
||||
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
|
||||
@@ -2740,6 +2751,7 @@ const I18N = {
|
||||
sizeConflictWarn:"Строка {line}: подзадачи вместе превышают указанный размер ({size}) — даже при самой оптимистичной оценке.",
|
||||
sizeConflictTooltip:"Подзадачи вместе превышают указанный размер",
|
||||
cheapApproxWarn:"Слишком много связанных групп альтернатив для точного поиска — самый дешёвый путь оценён жадно (локальный выбор в каждой группе).",
|
||||
assigneeOverloadWarn:"@{tag} несёт {share} % открытой работы на самом дешёвом пути ({stations} из {total} станций) — возможное узкое место.",
|
||||
st_idee:"идея", st_geplant:"запланировано", st_arbeit:"в работе", st_durchstich:"сквозной прототип",
|
||||
st_fertig:"готово", st_prod:"в эксплуатации", st_highrisk:"высокий риск", st_verworfen:"отклонено",
|
||||
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
|
||||
@@ -2844,6 +2856,7 @@ const I18N = {
|
||||
sizeConflictWarn:"पंक्ति {line}: उप-पैकेज मिलकर दिए गए आकार ({size}) से बड़े हैं — सबसे आशावादी आकलन में भी।",
|
||||
sizeConflictTooltip:"उप-पैकेज मिलकर दिए गए आकार से बड़े हैं",
|
||||
cheapApproxWarn:"सटीक खोज के लिए बहुत सारे युग्मित विकल्प-समूह — सबसे सस्ता पथ लालची अनुमान है (प्रति समूह स्थानीय चयन)।",
|
||||
assigneeOverloadWarn:"@{tag} सबसे सस्ते पथ पर खुले काम का {share}% उठाए हुए है ({total} में से {stations} स्टेशन) — संभावित अड़चन।",
|
||||
st_idee:"विचार", st_geplant:"नियोजित", st_arbeit:"प्रगति पर", st_durchstich:"कार्यशील ढाँचा",
|
||||
st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_highrisk:"उच्च जोखिम", st_verworfen:"अस्वीकृत",
|
||||
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
|
||||
@@ -2948,6 +2961,7 @@ const I18N = {
|
||||
sizeConflictWarn:"第 {line} 行:子项合计超出所标注的尺寸({size})——即使按最乐观的估算也是如此。",
|
||||
sizeConflictTooltip:"子项合计超出所标注的尺寸",
|
||||
cheapApproxWarn:"耦合的备选组过多,无法精确搜索——最便宜路径为贪心估计(每组就地选择)。",
|
||||
assigneeOverloadWarn:"@{tag} 承担最便宜路径上 {share}% 的未完成工作({total} 个站点中的 {stations} 个)——可能的瓶颈。",
|
||||
st_idee:"想法", st_geplant:"已计划", st_arbeit:"进行中", st_durchstich:"可运行骨架",
|
||||
st_fertig:"已完成", st_prod:"已上线", st_highrisk:"高风险", st_verworfen:"已放弃",
|
||||
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
|
||||
@@ -3052,6 +3066,7 @@ const I18N = {
|
||||
sizeConflictWarn:"{line} 行目:サブパッケージの合計が指定サイズ({size})を超えています — 最も楽観的な見積もりでも。",
|
||||
sizeConflictTooltip:"サブパッケージの合計が指定サイズを超えています",
|
||||
cheapApproxWarn:"結合された選択肢グループが多すぎるため厳密探索は不可 — 最安パスは貪欲法による推定です(グループごとに局所選択)。",
|
||||
assigneeOverloadWarn:"@{tag} が最安パスの未完了作業の {share}% を担っています(全 {total} 駅中 {stations} 駅)— ボトルネックの可能性。",
|
||||
st_idee:"アイデア", st_geplant:"計画済み", st_arbeit:"作業中", st_durchstich:"ウォーキングスケルトン",
|
||||
st_fertig:"完了", st_prod:"本番稼働", st_highrisk:"高リスク", st_verworfen:"破棄",
|
||||
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
|
||||
|
||||
@@ -318,6 +318,59 @@ export function cheapCls(n, cheapSet, collapsed){
|
||||
return hidesOpenCheap(n, cheapSet) ? 'cheap' : 'cheap cheap-leaf';
|
||||
}
|
||||
|
||||
/* ---------- Zuständigen-Engpass (SPEC §7/§9, D71) ----------
|
||||
Wer trägt wie viel der OFFENEN Arbeit des günstigsten Pfads? Jeder nötige
|
||||
Knoten zählt mit seiner Marge — dem, was seine Größe über die nötigen
|
||||
Teilpakete hinaus behauptet (dasselbe Maß wie der Belegungs-Vergleich in
|
||||
`computeCheapPlan`, D69); Erledigtes hat `ownCost` 0 und trägt damit von
|
||||
selbst nichts. Zuständig ist, wer auf der Zeile getaggt ist; ein Knoten
|
||||
OHNE Tags erbt für diese Rechnung die Tags des nächsten Vorfahren mit Tags
|
||||
(WBS-Lesart: Wer das Paket hat, hat die Teilpakete). Mehrere Tags teilen
|
||||
sich den Beitrag zu gleichen Teilen. Beiträge ohne getaggten Vorfahren
|
||||
gehen nur in die Gesamtsumme ein — sie verwässern die Anteile, warnen aber
|
||||
nicht selbst. Stationen (die tiefsten offenen Pfad-Knoten, D46) werden für
|
||||
den Meldungstext mitgezählt; ein Knoten mit mehreren Zuständigen zählt als
|
||||
Station bei jedem von ihnen. */
|
||||
export function assigneeLoads(roots, cheapSet){
|
||||
const loads = new Map(), stations = new Map();
|
||||
let total = 0, totalStations = 0;
|
||||
const walk = (ns, inherited) => {
|
||||
for(const n of ns){
|
||||
const owners = n.tags && n.tags.length ? n.tags : inherited;
|
||||
if(cheapSet.has(n)){
|
||||
let kids = 0;
|
||||
for(const k of pathChildren(n)) if(cheapSet.has(k)) kids += ownCost(k);
|
||||
const m = Math.max(0, ownCost(n) - kids);
|
||||
total += m;
|
||||
if(m > 0) for(const tag of owners)
|
||||
loads.set(tag, (loads.get(tag) || 0) + m / owners.length);
|
||||
if(!isDone(n) && !hidesOpenCheap(n, cheapSet)){
|
||||
totalStations++;
|
||||
for(const tag of owners) stations.set(tag, (stations.get(tag) || 0) + 1);
|
||||
}
|
||||
}
|
||||
walk(n.children, owners);
|
||||
}
|
||||
};
|
||||
walk(roots, []);
|
||||
return {loads, total, stations, totalStations};
|
||||
}
|
||||
/* Die Engpass-Entscheidung (SPEC §9, D71): gemeldet nur, wenn mindestens ZWEI
|
||||
Personen Last auf der offenen Front tragen — ein Solo-Plan warnt nie, eine
|
||||
Person mit allem ist dort keine Engstelle, sondern die Realität — und eine
|
||||
davon MEHR ALS DIE HÄLFTE der gesamten offenen Pfad-Arbeit hält (strikt;
|
||||
mehr als eine Person über der Hälfte kann es nicht geben). Die Schwelle ist
|
||||
gesetzt, nicht hergeleitet (D64-Linie). */
|
||||
export function overloadedAssignee(roots, cheapSet){
|
||||
const {loads, total, stations, totalStations} = assigneeLoads(roots, cheapSet);
|
||||
if(loads.size < 2 || total <= 0) return null;
|
||||
let tag = null, load = 0;
|
||||
loads.forEach((l, name) => { if(l > load){ load = l; tag = name; } });
|
||||
if(load * 2 <= total) return null;
|
||||
return {tag, share: Math.round(load / total * 100),
|
||||
stations: stations.get(tag) || 0, totalStations};
|
||||
}
|
||||
|
||||
/* ---------- Effektiver Status (SPEC §4/§9, D39) ----------
|
||||
Fortschritts-Rang entlang der Ergebnis-Skala (D5). Außerhalb der Skala:
|
||||
neutrale und verworfene Knoten zählen als 0 („nichts Anrechenbares"),
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
collapsedSet, // optional: eingeklappte Knoten (Faltung, SPEC §9/D38)
|
||||
effStatus, // optional: Map Knoten -> effektiver Status-Key, nur
|
||||
// Diskrepanzen (effectiveStatus() in model.js, D39)
|
||||
overloadTag, // optional: Person, die mehr als die Hälfte der offenen
|
||||
// Pfad-Arbeit trägt (overloadedAssignee(), SPEC §9/D71) —
|
||||
// ihre Pillen an offenen Pfad-Knoten werden warnfarben
|
||||
} */
|
||||
|
||||
import { gateOf, needsBreakdown, visibleChildren, cheapCls, isDone, assumedSize } from './model.js';
|
||||
@@ -190,8 +193,13 @@ function nodeHtml(n, extra, opts, fold){
|
||||
als zweiten, dürftigeren Tooltip. Kein zweites Attribut mit demselben Text
|
||||
— es ist dasselbe, nur unter anderem Namen. */
|
||||
const title = ` data-tip="${attr(tip)}"`;
|
||||
/* Zuständigen-Engpass (SPEC §9/D71): Die Pille der überlasteten Person
|
||||
wechselt an OFFENEN Pfad-Knoten auf die Warnfarbe — dort liegt die Last.
|
||||
Nur direkte Tags haben eine Pille; geerbte Zuständigkeit (die in die
|
||||
Rechnung eingeht) hat nichts, das sich färben ließe. */
|
||||
const hot = tag => opts.overloadTag === tag && opts.cheapSet.has(n) && !isDone(n);
|
||||
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>`
|
||||
? `<span class="tags" aria-hidden="true">${n.tags.map(tag => `<span class="tag${hot(tag) ? ' overload' : ''}">${esc(tag)}</span>`).join('')}</span>`
|
||||
: '';
|
||||
/* Das implizite Größen-Badge macht eine KOSTENANNAHME sichtbar (D18) —
|
||||
seit D66 die aus den Teilpaketen geschätzte Größe statt pauschal M. An
|
||||
|
||||
@@ -885,6 +885,10 @@
|
||||
border-radius:999px;padding:1px 6px;line-height:1.4;
|
||||
box-shadow:0 0 0 1.5px var(--card);
|
||||
}
|
||||
/* Zuständigen-Engpass (SPEC §9/D71): die Pille der Person, die mehr als die
|
||||
Hälfte der offenen Pfad-Arbeit trägt — Warnfarbe wie das
|
||||
Größen-Konflikt-Badge (D62); der Export liest die gemessenen Farben. */
|
||||
.tag.overload{background:var(--warn);color:#fff;border-color:var(--warn)}
|
||||
.ghost-node{
|
||||
position:relative;margin-top:14px;
|
||||
border:1.5px dashed var(--warn);
|
||||
|
||||
@@ -29,6 +29,11 @@
|
||||
angegebene Größe des Elternknotens, selbst
|
||||
in der günstigsten Bereichs-Lesart
|
||||
(SPEC §5/D62); Zeile = Elternknoten
|
||||
- assigneeOverload { tag, share, stations, totalStations }
|
||||
— eine Person trägt mehr als die Hälfte der
|
||||
offenen Arbeit des günstigsten Pfads
|
||||
(SPEC §9/D71); zeilenlos — der Engpass
|
||||
hat keine einzelne Zeile
|
||||
- cheapApprox { } — günstigster Pfad nur gierig geschätzt:
|
||||
zu viele gekoppelte Gruppen für die
|
||||
exakte Suche (D42); zeilenlos
|
||||
@@ -78,6 +83,9 @@ function build(w, t, esc){
|
||||
return t('descStrayWarn', {line: w.line});
|
||||
case 'sizeConflict':
|
||||
return t('sizeConflictWarn', {line: w.line, size: esc(w.size)});
|
||||
case 'assigneeOverload':
|
||||
return t('assigneeOverloadWarn', {tag: esc(w.tag), share: w.share,
|
||||
stations: w.stations, total: w.totalStations});
|
||||
case 'cheapApprox':
|
||||
return t('cheapApproxWarn');
|
||||
case 'sourceLoad':
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parse } from '../src/parser.js';
|
||||
import { computeCheapSet, assigneeLoads, overloadedAssignee } from '../src/model.js';
|
||||
import { renderTreeHtml } from '../src/render.js';
|
||||
import { formatWarning } from '../src/warnings.js';
|
||||
|
||||
/* Zuständigen-Engpass (SPEC §7/§9, D71): Trägt eine Person mehr als die Hälfte
|
||||
der offenen Arbeit des günstigsten Pfads (Marginalkosten-Maß, D69) und gibt
|
||||
es mindestens zwei Personen mit Last auf der offenen Front, wird gewarnt.
|
||||
Knoten ohne Tags erben für die Rechnung vom nächsten getaggten Vorfahren;
|
||||
mehrere Tags teilen sich den Beitrag. Kostenskala: XS=1 S=2 M=3 L=4 XL=5. */
|
||||
|
||||
const roots = txt => parse(txt).roots;
|
||||
const plan = txt => { const r = roots(txt); return { r, set: computeCheapSet(r) }; };
|
||||
const t = (key, vars) => {
|
||||
let s = key;
|
||||
if(vars) for(const k in vars) s += ':' + vars[k];
|
||||
return s;
|
||||
};
|
||||
|
||||
describe('overloadedAssignee — die Engpass-Entscheidung', () => {
|
||||
it('eine Person über der Hälfte, eine zweite auf der Front: Warnung', () => {
|
||||
const { r, set } = plan(`[ ] Plan
|
||||
- [ ] A (L) @anna
|
||||
- [ ] B (XS) @ben`);
|
||||
/* anna 4, ben 1, Wurzel geschätzt L mit Marge 0 ⇒ total 5, anna 80 % */
|
||||
expect(overloadedAssignee(r, set)).toEqual(
|
||||
{tag: 'anna', share: 80, stations: 1, totalStations: 2});
|
||||
});
|
||||
|
||||
it('ein Solo-Plan warnt nie — auch bei 100 %', () => {
|
||||
const { r, set } = plan(`[ ] Plan
|
||||
- [ ] A (L) @anna
|
||||
- [ ] B (S) @anna`);
|
||||
expect(overloadedAssignee(r, set)).toBeNull();
|
||||
});
|
||||
|
||||
it('genau die Hälfte reicht nicht — die Schwelle ist strikt', () => {
|
||||
const { r, set } = plan(`[ ] Plan
|
||||
- [ ] A (L) @anna
|
||||
- [ ] B (L) @ben`);
|
||||
expect(overloadedAssignee(r, set)).toBeNull();
|
||||
});
|
||||
|
||||
it('Knoten ohne Tags erben vom nächsten Vorfahren mit Tags', () => {
|
||||
const { r, set } = plan(`[ ] Plan
|
||||
- [ ] Paket (L) @anna
|
||||
- [ ] Teil (M)
|
||||
- [ ] B (S) @ben`);
|
||||
/* Teil (3) gehört per Vererbung anna; dazu Paket-Marge 1 ⇒ anna 4 von 6.
|
||||
Ohne Vererbung wäre anna nur die Marge 1 und niemand über der Hälfte. */
|
||||
const o = overloadedAssignee(r, set);
|
||||
expect(o && o.tag).toBe('anna');
|
||||
expect(o.share).toBe(67);
|
||||
});
|
||||
|
||||
it('mehrere Tags einer Zeile teilen sich den Beitrag', () => {
|
||||
const { r, set } = plan(`[ ] Plan
|
||||
- [ ] A (L) @anna @ben
|
||||
- [ ] C (S) @carla`);
|
||||
/* A: je 2 für anna und ben; voll doppelt gezählt läge anna bei 4 von 6
|
||||
und würde fälschlich gemeldet. */
|
||||
expect(overloadedAssignee(r, set)).toBeNull();
|
||||
});
|
||||
|
||||
it('Erledigtes trägt keine Last — die offene Front zählt', () => {
|
||||
const { r, set } = plan(`[ ] Plan
|
||||
- [x] Big (XL) @ben
|
||||
- [ ] A (S) @anna
|
||||
- [ ] B (XS) @ben`);
|
||||
/* bens großer Brocken ist fertig (0); offen sind anna 2 und ben 1 */
|
||||
const o = overloadedAssignee(r, set);
|
||||
expect(o && o.tag).toBe('anna');
|
||||
});
|
||||
|
||||
it('nicht zugewiesene Arbeit verwässert die Anteile, warnt aber nicht', () => {
|
||||
const { r, set } = plan(`[ ] Plan (XXL)
|
||||
- [ ] A (S) @anna
|
||||
- [ ] B (XS) @ben`);
|
||||
/* Wurzel-Marge 3 gehört niemandem ⇒ anna 2 von 6, unter der Hälfte */
|
||||
expect(overloadedAssignee(r, set)).toBeNull();
|
||||
});
|
||||
|
||||
it('leere Pfadmenge (Pfad aus): keine Aussage', () => {
|
||||
const r = roots(`[ ] Plan
|
||||
- [ ] A (L) @anna
|
||||
- [ ] B (XS) @ben`);
|
||||
expect(overloadedAssignee(r, new Set())).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('assigneeLoads — das Maß dahinter', () => {
|
||||
it('Marginalkosten je Person, Summe ist der Pfadpreis', () => {
|
||||
const { r, set } = plan(`[ ] Plan
|
||||
- [ ] A (L) @anna
|
||||
- [ ] B (XS) @ben`);
|
||||
const { loads, total } = assigneeLoads(r, set);
|
||||
expect(loads.get('anna')).toBe(4);
|
||||
expect(loads.get('ben')).toBe(1);
|
||||
expect(total).toBe(5);
|
||||
});
|
||||
|
||||
it('Stationen zählen nur offene Blätter des Pfads', () => {
|
||||
const { r, set } = plan(`[ ] Plan
|
||||
- [x] Fertig (S) @anna
|
||||
- [ ] Offen (S) @anna
|
||||
- [ ] B (XS) @ben`);
|
||||
const { stations, totalStations } = assigneeLoads(r, set);
|
||||
expect(stations.get('anna')).toBe(1);
|
||||
expect(totalStations).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Darstellung', () => {
|
||||
it('die Pille der überlasteten Person wird an offenen Pfad-Knoten markiert', () => {
|
||||
const { r, set } = plan(`[ ] Plan
|
||||
- [ ] A (L) @anna
|
||||
- [x] Done (S) @anna
|
||||
- [ ] B (XS) @ben`);
|
||||
const { html } = renderTreeHtml(r, {t, showDiscarded: false, cheapPath: true,
|
||||
cheapSet: set, overloadTag: 'anna'});
|
||||
/* genau eine markierte Pille: am offenen A — nicht am erledigten Done,
|
||||
nicht an bens Pille */
|
||||
expect(html.match(/tag overload/g)).toHaveLength(1);
|
||||
expect(html).toContain('<span class="tag overload">anna</span>');
|
||||
expect(html).toContain('<span class="tag">ben</span>');
|
||||
});
|
||||
|
||||
it('ohne overloadTag bleibt alles wie bisher', () => {
|
||||
const { r, set } = plan(`[ ] Plan
|
||||
- [ ] A (L) @anna
|
||||
- [ ] B (XS) @ben`);
|
||||
const { html } = renderTreeHtml(r, {t, showDiscarded: false, cheapPath: true,
|
||||
cheapSet: set, overloadTag: null});
|
||||
expect(html).not.toContain('overload');
|
||||
});
|
||||
|
||||
it('die Warnung nennt Person, Anteil und Stationen', () => {
|
||||
const msg = formatWarning(
|
||||
{type: 'assigneeOverload', tag: 'anna', share: 80, stations: 1, totalStations: 2}, t);
|
||||
expect(msg).toBe('assigneeOverloadWarn:anna:80:1:2');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user