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
@@ -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