From 0de7f5f24e2239c8e129e53bb536d2aa8f8a3634 Mon Sep 17 00:00:00 2001 From: mhoennig Date: Tue, 25 Aug 2026 11:04:26 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Die=20gesch=C3=A4tzte=20Gr=C3=B6=C3=9Fe?= =?UTF-8?q?=20bepreist=20den=20Rest=20=E2=80=94=20Erledigtes=20f=C3=A4llt?= =?UTF-8?q?=20aus=20der=20Sch=C3=A4tzung=20(D70)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/CHANGELOG.md | 1 + docs/DECISIONS.md | 49 ++++++++++++++++++++++++++++++ docs/SPEC.md | 10 +++++++ frontend/public/llms.md | 4 +++ frontend/src/model.js | 25 ++++++++++++++-- frontend/tests/assumed.test.js | 54 ++++++++++++++++++++++++++++++++++ 6 files changed, 140 insertions(+), 3 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b37232a..12f7bde 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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 diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 2ef9cd3..a9cdeac 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -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. diff --git a/docs/SPEC.md b/docs/SPEC.md index 24c0d8f..2cb2fd4 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -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 diff --git a/frontend/public/llms.md b/frontend/public/llms.md index 3860e9c..fb29dc7 100644 --- a/frontend/public/llms.md +++ b/frontend/public/llms.md @@ -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): diff --git a/frontend/src/model.js b/frontend/src/model.js index 19a698b..1872381 100644 --- a/frontend/src/model.js +++ b/frontend/src/model.js @@ -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; diff --git a/frontend/tests/assumed.test.js b/frontend/tests/assumed.test.js index 89864d2..1ae7dea 100644 --- a/frontend/tests/assumed.test.js +++ b/frontend/tests/assumed.test.js @@ -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