feat: Größen-Konflikt — Teilpakete müssen zur angegebenen Größe passen (D62)

Größen zählen für diese eine Prüfung als Bereiche (Untergrenzen verdoppeln
sich, XXL nach oben offen); Konflikt erst, wenn selbst die günstigste Lesart
der Kinder die großzügigste des Elternknotens erreicht. Warnung sizeConflict
an der Elternzeile, Badge in --warn, Grund in Tooltip und aria-label. Nichts
wird automatisch korrigiert. Nebenbefund behoben: der Grafikexport zeichnete
das Größen-Badge mit festen Farben — das invertierte implizite M stand seit
jeher gefüllt im Bild. Die acht berechtigten Konflikte im eigenen Plan sind
nachgezogen. 372 Tests, 16 neue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-24 15:55:16 +02:00
co-authored by Claude Fable 5
parent 257e1a9c17
commit 5b37c117e1
11 changed files with 305 additions and 11 deletions
+2
View File
@@ -19,6 +19,8 @@ reverse.
## 2026-08-24
- A size that cannot hold its sub-packages turns its badge amber and gets a warning — sizes are read as ranges, so only sure conflicts are reported
- Fix: the exported SVG drew the assumed-M badge filled like a real size instead of inverted
- Dependency links now run behind every node, highlighted ones included — no more lines struck through node titles
- Fix: the line to an only child was torn where the child does not sit centred in its cell
- An optional node (`+`) joins the cheapest path while it is being worked on — started work is the open front
+71
View File
@@ -4582,3 +4582,74 @@ heraus, fallen genau die fünf danach benannten Zusicherungen; nimmt man
`chosenPool` heraus, genau die zwei zu den Alternativen. Die Tests, die das
**unveränderte** Verhalten festhalten (unangetastete und erledigte Zugaben
bleiben draußen), bleiben in beiden Fällen grün.
## D62 — Größen-Konflikt: Bereiche statt Punkt-Zahlen, XXL nach oben offen
Gewünscht war eine Anzeige, „wenn die angegebenen T-Shirt-Größen mit den
Unterknoten nicht zusammenpassen" — ohne automatische Korrektur. Die Skala
dafür gab es schon (`SIZE_RANK`, D18 nutzt Rang+1 als Kosten); die eigentliche
Entscheidung war, **wann** „passt nicht" gilt.
**Eine Punkt-Zahl je Größe trägt nicht — egal wie steil die Skala steigt.**
Der erste Vorschlag war eine additive Verdopplungsskala (XS=1 … XXL=32):
Summe der Kinder > Elternwert ⇒ Konflikt. Der Nutzer wandte ein, dass damit
bei jedem größeren Baum die oberen Knoten selbst mit XXL nicht auskommen —
und die Prüfung der Steilheit führte auf einen härteren Befund: Schon das
**kanonische Beispiel aus SPEC §10** warnte. `Website-Relaunch (XL)` mit
Kindern `(M) + (XL) + (M)` ergibt bei Faktor 2 wie bei Faktor 3 eine Summe
über dem Elternwert — sobald ein Kind die Elterngröße teilt, sprengt jedes
Geschwister die Summe. Das widerspricht der gelebten Praxis, dass „XL =
XL-Kind plus etwas Kleinkram" eine völlig normale Schätzung ist. Der Fehler
liegt nicht im Faktor, sondern darin, dass eine Punkt-Zahl so tut, als wäre
`(M)` exakt.
**Entschieden: Größen sind Bereiche.** XS=[1,2), S=[2,4), M=[4,8), L=[8,16),
XL=[16,32), **XXL=[32,∞)**. Konflikt erst, wenn die Summe der **Untergrenzen**
der Kinder die **Obergrenze** des Elternknotens erreicht — also erst, wenn es
unter *jeder* Lesart falsch ist. Das hält das kanonische Beispiel sauber
(24 < 32), meldet vier `(S)` unter `(M)` (8 ≥ 8), enthält den Ordinal-Fall
(ein strikt größeres Kind warnt immer) und beantwortet den Einwand direkt:
**XXL hat keine Obergrenze**, ein XXL-Knoten warnt nie — für die großen
Sammelknoten behauptet die Skala schlicht keine Schranke mehr. Das ist
dieselbe Haltung wie bei `unknownStatus` und `descStray`: lieber laut, aber
nur, wo es sicher ist. D46 („die Größen sind ordinal, nicht additiv") bleibt
für alles andere unangetastet — die Bereichs-Lesart gilt genau dieser einen
Prüfung.
**Was zählt:** nur die direkten Kinder (je Ebene eine eigene Prüfung, die
Warnung zeigt auf die Elternzeile), davon nur die mit **angegebener** Größe —
fehlende Größe ist keine Autoren-Aussage (D44-Linie; anders als bei den
Pfadkosten wird kein M angenommen). Verworfene und optionale (`+`) Kinder
zählen nicht; in einer disjunktiven Gruppe (`|`/`=`) zählt die **kleinste**
Alternative, denn nur eine wird realisiert. Die Gegenrichtung (Eltern größer
als die Kindersumme) warnt nicht — sie heißt nur unvollständige Zerlegung,
und dafür gibt es den Geister-Knoten (D8). Ein Elternknoten ohne Größe wird
nie geprüft.
**Darstellung: das Badge wechselt auf `--warn`, nicht auf Rot.** Der Nutzer
hatte Rot vorgeschlagen und sich für die Warnfarbe entschieden: „Der Plan
widerspricht sich" hat mit `#B45309` schon eine Farbe (Geister-Knoten,
Warnzeilen), und Rot bliebe unvergeben (D34). Dazu die Warnung `sizeConflict`
mit Zeilennummer (eine Stelle: `build()` in warnings.js), der Grund im
Tooltip und im `aria-label`. **Nichts wird automatisch korrigiert.**
**Nebenbefund: Der Grafikexport zeichnete das Größen-Badge mit festen
Farben** (`drawBadge(sizeEl, '#0F766E', '#ffffff')`) — das invertierte
implizite M (D18) stand damit seit jeher gefüllt im exportierten Bild. Jetzt
liest der Export die gemessenen Farben wie bei Tags und Diskrepanz-Marke;
damit folgt auch das Konflikt-Badge von selbst.
**Der eigene Plan hatte acht solcher Konflikte** — alle berechtigt, die
Größen stammten aus der Zeit ohne Prüfung. Nachgezogen (`#not` L→XL, `#ed`
XL→XXL, `#ed.live`/`#ed.path`/`#ed.fold`/`#bld`/`#col.own` M→L, `#col.pad`
S→L, und als Kaskade davon `#col` XL→XXL): 0 Warnungen. Dass die erste
Anwendung der Regel den eigenen Plan korrigiert, ist kein schlechtes Zeichen —
genau dafür ist sie da.
**Nachgemessen:** 372 Tests (16 neue in `tests/sizes.test.js`); Gegenprobe
per Mutation — XXL-Obergrenze wieder eingeführt: genau die zwei danach
benannten Zusicherungen fallen; optionale Kinder mitgezählt: genau eine;
disjunktive Gruppen summiert statt Minimum: genau zwei. Im Browser: Badge
`rgb(180, 83, 9)`, Warnung „Zeile 1: …", Tooltip und `aria-label` benennen
den Grund; im exportierten SVG das Konflikt-Badge bernstein und das implizite
M weiß mit Petrol-Rand. Kanonisches Beispiel §10 und der mitgelieferte Plan:
0 Warnungen.
+32
View File
@@ -325,6 +325,38 @@ effektiv nicht weiter sein als das, was er braucht.
- Anzeige: petrolfarbenes Badge (`--or`, `#0F766E`) mit weißer Schrift oben
rechts an der Knoten-Ecke.
**Größen-Konflikt** — die angegebene Größe eines Elements muss zu seinen
Teilpaketen passen. Die Skala ist ordinal; für diese eine Prüfung wird jede
Größe als **Bereich** gelesen (Untergrenzen verdoppeln sich):
| | XS | S | M | L | XL | XXL |
|---|---|---|---|---|---|---|
| Bereich | [1, 2) | [2, 4) | [4, 8) | [8, 16) | [16, 32) | [32, ∞) |
- **Konflikt** ist, wenn selbst die **günstigste Lesart der Kinder** die
**großzügigste Lesart des Elternknotens** erreicht: Summe der Untergrenzen
der zählenden Kinder ≥ Obergrenze der Elterngröße. Gemeldet wird also nur,
was unter *jeder* Lesart falsch ist — `(XL) = XL-Kind + 2 × M` (16+4+4 = 24
< 32) ist deshalb kein Konflikt, vier `(S)` unter einem `(M)` (8 ≥ 8) schon.
Ein Kind, das **größer** ist als sein Elternknoten, ist immer ein Konflikt
(Teilmenge der Regel).
- **XXL hat keine Obergrenze** — ein XXL-Element warnt nie: Für die großen
Sammelknoten eines Plans behauptet die Skala schlicht keine Schranke mehr.
- **Es zählen nur die direkten Kinder**, und davon nur die mit **angegebener**
Größe (fehlende Größe ist keine Aussage; anders als beim günstigsten Pfad
wird hier kein `M` angenommen). Verworfene und optionale (`+`) Kinder zählen
nicht. In einer disjunktiven Gruppe (`|`/`=`) wird nur eine Alternative
realisiert — dort zählt die **kleinste**.
- Ein Elternknoten **ohne** Größe macht keine Aussage und wird nie geprüft;
die Gegenrichtung (Eltern größer als die Kindersumme) warnt nicht — sie
heißt nur, dass die Zerlegung unvollständig ist (dafür gibt es den
Geister-Knoten).
- **Meldung und Anzeige:** Warnung `sizeConflict` mit der Zeilennummer des
Elternknotens; sein Größen-Badge wechselt auf die Warnfarbe (`--warn`,
`#B45309`, weiße Schrift), Tooltip und `aria-label` benennen den Grund.
Badge-Färbung auch im Grafikexport und im Druck. **Nichts wird automatisch
korrigiert** — die Größen bleiben, wie sie geschrieben sind. Siehe D62.
## 6. Links
- Ein nacktes `https://…`-Token macht den ganzen Knoten klickbar
+16 -9
View File
@@ -3,7 +3,7 @@
%% Every node carries an id; the block behind --- explains it. Hover a node.
[~] #wb: Werkbaum (XXL) https://github.com/mhoennig/werkbaum
- [^] #not: Notation (L)
- [^] #not: Notation (XL)
- [^] #not.line: Line format (M)
- [^] #not.status: Status box, eight states (S)
- [^] #not.size: T-shirt size and the "decompose from M" rule (S)
@@ -23,6 +23,7 @@
- [^] #not.desc: Node descriptions, shown as a tooltip (M)
- [^] #not.desc.short: A short text right at the node (S) %% a " line below the node
- [^] #not.desc.long: A long block at the end, addressed by its id (S) %% behind a --- separator
- [x] #not.sizes: Size conflict — sub-packages must fit the given size (S) %% ranges, XXL open-ended, see D62
- [-] #not.store: A separate storage format for the structure (L) %% the text is the format
- [ ] #not.ticket: Ticket references (#123,#US-123) (S) %% Taiga writes #US-123 for a user story
- [^] #not.llms: llms.md, the notation explained to AI agents (S)
@@ -30,8 +31,8 @@
- [?] #not.benefit: Benefit per node, not only cost (M)
| [?] #not.benefit.sigil: Another sigil next to the size (S)
| [?] #not.benefit.points: Story points behind the T-shirt sizes (M)
- [~] #ed: Editor (XL) https://werkbaum.javagil.de
- [^] #ed.live: Live diagram from text (M)
- [~] #ed: Editor (XXL) https://werkbaum.javagil.de
- [^] #ed.live: Live diagram from text (L)
- [^] #ed.parser: Parser (S)
- [^] #ed.render: Renderer, three layout modes (M)
- [^] #ed.render.horiz: Horizontal fan (S)
@@ -40,7 +41,7 @@
- [^] #ed.deplink: Cross links for dependencies (M)
- [^] #ed.deplink.calm: Visually secondary — the tree carries the message (S)
- [^] #ed.deplink.focus: Highlight what a selected node needs and feeds (S)
- [^] #ed.path: Cheapest path (M)
- [^] #ed.path: Cheapest path (L)
- [^] #ed.path.cost: Cost model, missing size counts as M (S)
- [^] #ed.path.front: Only the open front: what is done costs nothing (S) %% see D46
- [^] #ed.path.started: Started work is on the path, extras included (S) %% see D61
@@ -73,7 +74,7 @@
- [^] #ed.shortid: Type #.kc under #prod-stage, get #prod-stage.kc (XS) %% an input aid, not notation
- [^] #ed.showids: A # button shows the ids before the titles (XS) %% written as in the text, with a colon
- [^] #ed.lineno.tip: The warning is the tooltip of its number (XS) %% no hunting in the list
- [^] #ed.fold: Collapsible subtrees (M)
- [^] #ed.fold: Collapsible subtrees (L)
- [^] #ed.fold.marks: Initial state from the text: > collapses, < re-opens (S)
- [^] #ed.fold.click: Fold and unfold in the diagram (S)
- [^] #ed.fold.write: Folding writes the mark back into the text (S) %% one undo step
@@ -87,7 +88,7 @@
- [?] #ed.dates: Dates and milestones (M)
| [?] #ed.dates.attr: An attribute in the line (S)
| [?] #ed.dates.view: A separate timeline view (L)
- [^] #bld: Build and delivery (M)
- [^] #bld: Build and delivery (L)
- [^] #bld.bundle: One self-contained file, bundled by Vite (S)
- [^] #bld.tests: Unit tests (S)
- [^] #bld.pages: Published on every push (S)
@@ -110,9 +111,9 @@
- [?] #be.auth: Accounts and permissions (L) :#be.docs
| [?] #be.auth.token: Single user, one token (S)
| [?] #be.auth.oidc: Log in with OIDC (L)
- [~] #col: Collaborating on a Diagram (XL)
- [~] #col: Collaborating on a Diagram (XXL)
" The pad merges concurrent edits, Werkbaum renders the plan.
- [^] #col.pad: Watch a shared Etherpad — it merges, we render (S)
- [^] #col.pad: Watch a shared Etherpad — it merges, we render (L)
- [^] #col.pad.export: Load the pad's plain-text export (XS)
- [^] #col.pad.reload: Reload on demand (XS) %% a timer only trips Etherpad's rate limit
- [^] #col.pad.embed: Embed the pad, keep the text mirror beside it (M)
@@ -123,7 +124,7 @@
| [ ] #col.pad.owntab: Live with it: edit in the pad's own tab (XS)
- [^] #col.pad.point: A shared pointer: !!! focuses a node for everyone (XS)
- [?] > #col.auto: Update by itself, within about 2 s (M)
| [?] #col.own: Our own Etherpad beside Werkbaum (M) %% then every setting below is ours
| [?] #col.own: Our own Etherpad beside Werkbaum (L) %% then every setting below is ours
- [ ] #col.own.db: Node and a real database in userland (S) %% the flatfile store is not for production
- [ ] #col.own.ram: Book the RAM quota for an own daemon (XS)
- [ ] #col.own.proxy: Reverse proxy in .htaccess, websockets included (S) %% Mattermost does it, wiki shows how
@@ -295,6 +296,12 @@
are its text. There is no closing fence, so there is none to forget — this
section is its own example.
#not.sizes
Every size is read as a range whose lower bounds double, XXL open-ended. A
warning appears only when even the most optimistic reading of the children
exceeds the most generous reading of the parent — nothing is corrected
automatically, the badge just turns amber.
#not.store
Rejected on purpose: the text is the format. A second representation of the
structure would need syncing, and the first disagreement between the two