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:
mhoennig
2026-08-25 11:29:52 +02:00
co-authored by Claude Fable 5
parent d1da6a3529
commit 35b72fc178
10 changed files with 343 additions and 4 deletions
+53
View File
@@ -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"),