feat: Die geschätzte Größe bepreist den Rest — Erledigtes fällt aus der Schätzung (D70)

Die D66-Schätzung war status-blind: Ein größenloses Paket mit [x] (L) und
[ ] (S) wurde als L geschätzt, obwohl nur S offen ist. Erledigte Kinder
fallen jetzt heraus, eine erledigte realisierte Alternative stellt ihre
Gruppe fertig, und sind alle benannten Kinder erledigt, gilt XS (der eigene
Rest — M-Rückfall erhöhte den Preis beim Fertigwerden). Angegebene Größen
bleiben unangetastet (D69).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-25 11:04:26 +02:00
co-authored by Claude Fable 5
parent 4d80feb4e8
commit 0de7f5f24e
6 changed files with 140 additions and 3 deletions
+1
View File
@@ -19,6 +19,7 @@ reverse.
## 2026-08-25
- 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)`
- The size is now the last free-standing `(L)`-style token of the line — earlier ones stay in the title, and `"(L)"` or `((L))` mention a size literally
+49
View File
@@ -5170,3 +5170,52 @@ fallen; Vereinigungs-Maß auf schlichte Summe zurück → genau der eine
Marginal-Test (dessen erste Fassung nicht unterschied, weil Wurzeln immer
nötig sind — der Testbaum musste die Ziele unter einen unangetasteten
`+`-Zweig legen).
## D70 — Die geschätzte Größe bepreist den Rest: Erledigtes fällt aus der Schätzung
Nachgefragt vom Nutzer zum benannten D69-Verlust (erledigte Teilpakete
senken den Preis eines offenen Knotens nicht mehr): „Wenn der Überknoten
keine explizite, sondern nur eine implizit berechnete Größe hat, dann ist
das doch kein Problem?" Die Prüfung ergab: **Es war eines** — die
D66-Schätzung war status-blind. Ein größenloses Paket mit `[x] (L)` und
`[ ] (S)` wurde als L geschätzt (Preis 4), obwohl nur noch S offen ist.
**Die Unterscheidung des Nutzers ist genau die tragfähige Linie:** Eine
**angegebene** Größe ist die Aussage des Autors — D69 erklärt sie für
maßgeblich, dort bleibt der Verlust bewusst bestehen (anteilige Restkosten
hat D46 verworfen). Die **geschätzte** Größe ist dagegen eine Kostenannahme
des Werkzeugs, und der Pfad fragt seit D46 „was ist noch offen?" — eine
Annahme, die Erledigtes einpreist, beantwortet die falsche Frage.
Die Regeln:
- **Erledigte Kinder (`[x]`/`[^]`, intrinsisch) fallen aus der Schätzung**
wie verworfene — geschätzt wird die noch offene Arbeit, auch für die
Stufe-ab-drei-Regel zählen nur die offenen.
- **Disjunktiv stellt eine erledigte realisierte Alternative die Gruppe
fertig.** Kein eigener Sonderfall, sondern die Wahl des Pfads
nachvollzogen: Unter mehreren realisierten entscheidet die Kostenregel
(D61), und die erledigte kostet 0 — sie würde gewählt, die Gruppe trägt
nichts mehr bei. Das gilt auch neben einer angefangenen zweiten
Alternative.
- **Alles Benannte erledigt, der Knoten selbst offen → `XS`**, nicht der
M-Rückfall. Die Restarbeit ist dann seine eigene Abschlussarbeit (er ist
die Station, D46) — und der M-Rückfall erzeugte eine Absurdität: Das
Fertigstellen des letzten S-Kindes *erhöbe* den Preis von S auf M. Die
Schätzung muss beim Fertigwerden monoton sinken; XS ist der Boden der
Skala. Nur der echte Blattknoten ohne Kinder bleibt beim M-Rückfall —
dort gibt es keine Information.
**Sichtbare Nebenwirkung, gewollt:** Das invertierte Größen-Badge zeigt an
solchen Knoten die **Rest**-Schätzung. Der Tooltip sagt ohnehin „für die
Kostenschätzung mindestens {size} angenommen" — die Aussage bleibt wahr.
Größen-Konflikt (D62) und Falt-Voreinstellung (D44) arbeiten nur mit
angegebenen Größen und bleiben unberührt.
**Nachgemessen:** 457 Tests, davon 7 neue in `tests/assumed.test.js`
(darunter die Monotonie und die Zusicherung, dass eine ANGEGEBENE Größe
trotz erledigter Kinder stehen bleibt). Gegenproben per Mutation:
Done-Filter entfernt → genau die drei danach benannten Zusicherungen
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.
+10
View File
@@ -648,6 +648,16 @@ werden die für die günstigste Realisierung **nötigen** Knoten:
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.
**Erledigte Kinder zählen nicht mit** (D70): Die Schätzung ist eine
Kostenannahme des Werkzeugs, keine Autoren-Aussage — anders als eine
angegebene Größe (die bleibt, wie sie geschrieben ist) schätzt sie die
noch **offene** Arbeit. Kinder mit `[x]`/`[^]` (intrinsisch) fallen
heraus wie verworfene; in einer disjunktiven Gruppe stellt eine
**erledigte** realisierte Alternative die Gruppe fertig — sie trägt
nichts mehr bei. Gibt es zählende Kinder, sind aber **alle** erledigt,
wird **`XS`** angenommen: Die Restarbeit ist dann die eigene des
Elternknotens (er ist die Station, oben) — ein `M`-Rückfall hieße, dass
das Fertigstellen des letzten Kindes den Preis *erhöht*.
**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
+4
View File
@@ -120,6 +120,10 @@ One node per line. Everything except the label is optional.
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`.
The estimate prices the **remaining** work: done children (`[x]`/`[^]`)
drop out, and a done realized alternative completes its `|`/`=` group.
When counting children exist but all are done, `XS` is assumed — the rest
is the parent's own wrap-up. A stated size is never adjusted this way.
- **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):
+22 -3
View File
@@ -99,19 +99,38 @@ 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 =>
const counting = n.children.filter(k =>
!k.optional && (!k.status || k.status.key !== 'verworfen'));
/* Erledigte Kinder zählen nicht mit (D70): Die Schätzung ist eine
Kostenannahme, keine Autoren-Aussage sie schätzt die noch OFFENE
Arbeit (D46). Eine ANGEGEBENE Größe bleibt dagegen, wie sie geschrieben
ist (D69). In einer disjunktiven Gruppe stellt eine erledigte
realisierte Alternative die Gruppe fertig genau die würde der Pfad
wählen (Kosten 0), die Gruppe trägt also nichts mehr bei. */
let kids;
if(counting.length && gateOf(counting) !== 'and'){
const pool = chosenPool(counting);
kids = pool.some(isDone) ? [] : pool;
} else {
kids = counting.filter(k => !isDone(k));
}
let size = 'M';
if(kids.length){
if(gateOf(kids) !== 'and'){
if(gateOf(counting) !== '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)]))];
size = SIZE_BY_RANK[Math.min(...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)];
}
} else if(counting.length){
/* Alles Benannte ist erledigt, der Knoten selbst nicht: Der Rest ist
seine eigene Abschlussarbeit (er ist die Station, D46) XS statt des
M-Rückfalls, sonst ERHÖHTE das Fertigstellen des letzten Kindes den
Preis. Nur der echte Blattknoten ohne Kinder bleibt bei M. */
size = 'XS';
}
ASSUMED.set(n, size);
return size;
+54
View File
@@ -115,6 +115,60 @@ describe('assumedSize — Schätzung aus den Teilpaketen (D66)', () => {
});
});
describe('die Schätzung zählt nur offene Kinder (D70)', () => {
it('erledigte Kinder fallen heraus — geschätzt wird der Rest', () => {
const [n] = roots(`[ ] Paket
- [x] Großteil (L)
- [ ] Rest (S)`);
expect(assumedSize(n)).toBe('S');
});
it('die Stufe ab drei zählt nur die offenen', () => {
const [n] = roots(`[ ] Paket
- [x] A (S)
- [x] B (S)
- [ ] C (S)
- [ ] D (S)`);
expect(assumedSize(n)).toBe('S');
});
it('alles Benannte erledigt: XS — der eigene Rest, nicht der M-Rückfall', () => {
/* Mit M-Rückfall ERHÖHTE das Fertigstellen des letzten Kindes den
Preis von S auf M die Schätzung muss monoton sinken. */
const [n] = roots(`[ ] Paket
- [x] A (S)
- [x] B (S)`);
expect(assumedSize(n)).toBe('XS');
expect(ownCost(n)).toBe(1);
});
it('ein echtes Blatt ohne Kinder bleibt beim M-Rückfall', () => {
expect(assumedSize(roots('[ ] Blatt')[0])).toBe('M');
});
it('disjunktiv: eine erledigte realisierte Alternative stellt die Gruppe fertig', () => {
const [n] = roots(`[ ] Paket
| [x] Gemacht (S)
| [ ] Teuer (M)`);
expect(assumedSize(n)).toBe('XS');
});
it('disjunktiv: erledigt neben angefangen — die Gruppe trägt nichts mehr bei', () => {
/* Der Pfad wählte unter den realisierten die erledigte (Kosten 0, D61)
die Schätzung folgt derselben Wahl. */
const [n] = roots(`[ ] Paket
| [x] Fertig (L)
| [~] Angefangen (S)`);
expect(assumedSize(n)).toBe('XS');
});
it('eine ANGEGEBENE Größe bleibt trotz erledigter Kinder stehen (D69)', () => {
const [n] = roots(`[ ] Paket (L)
- [x] Großteil (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