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:
co-authored by
Claude Fable 5
parent
257e1a9c17
commit
5b37c117e1
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -109,6 +109,15 @@ One node per line. Everything except the label is optional.
|
||||
- From `(M)` upward a node **should be decomposed further**; a node ≥ M
|
||||
without children gets a placeholder hint in the diagram.
|
||||
- For cost estimation a missing size counts as `M`.
|
||||
- **Size conflict check:** a given size must fit the direct children. Each
|
||||
size is read as a range (lower bounds double: XS=1, S=2, M=4, L=8, XL=16,
|
||||
XXL=32; the upper bound is the next lower bound, XXL is open-ended). It is
|
||||
a conflict — warning `sizeConflict` at the parent's line — only when the sum
|
||||
of the children's lower bounds reaches the parent's upper bound, i.e. when
|
||||
it is wrong under *every* reading. Only sized, non-optional, non-discarded
|
||||
direct children count; in an `|`/`=` group only the smallest alternative.
|
||||
An XXL parent never warns; nothing is corrected automatically. When you
|
||||
write plans, pick parent sizes that pass this check.
|
||||
|
||||
### Token extraction order (why nothing collides)
|
||||
|
||||
|
||||
+28
-1
@@ -608,8 +608,17 @@ function diagramToSvg(){
|
||||
parts.push(`<circle cx="${rb.cx.toFixed(1)}" cy="${rb.cy.toFixed(1)}" r="${(Math.min(rb.w,rb.h)/2).toFixed(1)}" fill="#ffffff" stroke="#F97316" stroke-width="1.5"/>`);
|
||||
parts.push(`<text x="${rb.cx.toFixed(1)}" y="${(rb.cy+3.5).toFixed(1)}" text-anchor="middle" fill="#F97316" font-size="10">⚠︎</text>`);
|
||||
}
|
||||
/* Gemessene Farben statt festem Petrol: Das invertierte implizite M (D18)
|
||||
und das warnfarbene Konflikt-Badge (SPEC §5/D62) kämen sonst als
|
||||
gewöhnliches gefülltes Badge ins Bild — beim impliziten M war genau das
|
||||
seit jeher der Fall (Nebenbefund D62). */
|
||||
const sizeEl = node.querySelector('.size');
|
||||
if(sizeEl) drawBadge(sizeEl, '#0F766E', '#ffffff');
|
||||
if(sizeEl){
|
||||
const s = getComputedStyle(sizeEl);
|
||||
/* Der Rand gehört mit: beim invertierten Badge ist er die einzige Kontur
|
||||
(weiße Füllung auf weißer Karte). */
|
||||
drawBadge(sizeEl, s.backgroundColor, s.color, s.borderTopColor);
|
||||
}
|
||||
node.querySelectorAll('.tag').forEach(tg => {
|
||||
const t = getComputedStyle(tg);
|
||||
drawBadge(tg, t.backgroundColor, t.color, t.borderTopColor);
|
||||
@@ -1955,6 +1964,8 @@ const I18N = {
|
||||
unknownDepWarn:"Zeile {line}: Abhängigkeit #{id} — es gibt keinen Knoten mit dieser ID.",
|
||||
unknownDescWarn:"Zeile {line}: Beschreibung für #{id} — es gibt keinen Knoten mit dieser ID.",
|
||||
descStrayWarn:"Zeile {line}: Beschreibungszeile ohne Bezug — ihr fehlt der Knoten bzw. der #id-Block davor.",
|
||||
sizeConflictWarn:"Zeile {line}: Die Teilpakete übersteigen zusammen die angegebene Größe ({size}) — selbst in der günstigsten Lesart.",
|
||||
sizeConflictTooltip:"Die Teilpakete übersteigen zusammen die angegebene Größe",
|
||||
cheapApproxWarn:"Zu viele gekoppelte Alternativgruppen für die exakte Suche — der günstigste Pfad ist gierig geschätzt (je Gruppe lokal gewählt).",
|
||||
st_idee:"Idee", st_geplant:"geplant", st_arbeit:"in Arbeit", st_durchstich:"Durchstich",
|
||||
st_fertig:"fertig", st_prod:"in Produktion", st_highrisk:"High Risk", st_verworfen:"verworfen",
|
||||
@@ -2056,6 +2067,8 @@ const I18N = {
|
||||
unknownDepWarn:"Line {line}: dependency #{id} — no node has this ID.",
|
||||
unknownDescWarn:"Line {line}: description for #{id} — no node has this ID.",
|
||||
descStrayWarn:"Line {line}: description line with nothing to attach to — it needs a node or an #id block before it.",
|
||||
sizeConflictWarn:"Line {line}: the sub-packages together exceed the given size ({size}) — even in the most optimistic reading.",
|
||||
sizeConflictTooltip:"The sub-packages together exceed the given size",
|
||||
cheapApproxWarn:"Too many coupled alternative groups for the exact search — the cheapest path is a greedy estimate (chosen locally per group).",
|
||||
st_idee:"idea", st_geplant:"planned", st_arbeit:"in progress", st_durchstich:"walking skeleton",
|
||||
st_fertig:"done", st_prod:"in production", st_highrisk:"high risk", st_verworfen:"discarded",
|
||||
@@ -2157,6 +2170,8 @@ const I18N = {
|
||||
unknownDepWarn:"Línea {line}: dependencia #{id} — ningún nodo tiene esta ID.",
|
||||
unknownDescWarn:"Línea {line}: descripción para #{id} — ningún nodo tiene esta ID.",
|
||||
descStrayWarn:"Línea {line}: línea de descripción sin referencia — le falta un nodo o un bloque #id delante.",
|
||||
sizeConflictWarn:"Línea {line}: los subpaquetes juntos superan el tamaño indicado ({size}), incluso en la lectura más optimista.",
|
||||
sizeConflictTooltip:"Los subpaquetes juntos superan el tamaño indicado",
|
||||
cheapApproxWarn:"Demasiados grupos de alternativas acoplados para la búsqueda exacta — el camino más barato es una estimación voraz (elección local por grupo).",
|
||||
st_idee:"idea", st_geplant:"planificado", st_arbeit:"en curso", st_durchstich:"prototipo funcional",
|
||||
st_fertig:"terminado", st_prod:"en producción", st_highrisk:"alto riesgo", st_verworfen:"descartado",
|
||||
@@ -2258,6 +2273,8 @@ const I18N = {
|
||||
unknownDepWarn:"Ligne {line} : dépendance #{id} — aucun nœud ne porte cet ID.",
|
||||
unknownDescWarn:"Ligne {line} : description pour #{id} — aucun nœud ne porte cet ID.",
|
||||
descStrayWarn:"Ligne {line} : ligne de description sans rattachement — il lui manque un nœud ou un bloc #id avant.",
|
||||
sizeConflictWarn:"Ligne {line} : les sous-lots dépassent ensemble la taille indiquée ({size}), même dans la lecture la plus optimiste.",
|
||||
sizeConflictTooltip:"Les sous-lots dépassent ensemble la taille indiquée",
|
||||
cheapApproxWarn:"Trop de groupes d’alternatives couplés pour la recherche exacte — le chemin le moins cher est une estimation gloutonne (choix local par groupe).",
|
||||
st_idee:"idée", st_geplant:"planifié", st_arbeit:"en cours", st_durchstich:"squelette fonctionnel",
|
||||
st_fertig:"terminé", st_prod:"en production", st_highrisk:"risque élevé", st_verworfen:"abandonné",
|
||||
@@ -2359,6 +2376,8 @@ const I18N = {
|
||||
unknownDepWarn:"Wiersz {line}: zależność #{id} — żaden węzeł nie ma tego ID.",
|
||||
unknownDescWarn:"Wiersz {line}: opis dla #{id} — żaden węzeł nie ma tego ID.",
|
||||
descStrayWarn:"Wiersz {line}: wiersz opisu bez odniesienia — brakuje węzła lub bloku #id przed nim.",
|
||||
sizeConflictWarn:"Wiersz {line}: podzadania razem przekraczają podany rozmiar ({size}) — nawet w najkorzystniejszym odczycie.",
|
||||
sizeConflictTooltip:"Podzadania razem przekraczają podany rozmiar",
|
||||
cheapApproxWarn:"Zbyt wiele sprzężonych grup alternatyw dla dokładnego wyszukiwania — najtańsza ścieżka jest oszacowana zachłannie (wybór lokalny w każdej grupie).",
|
||||
st_idee:"pomysł", st_geplant:"zaplanowane", st_arbeit:"w toku", st_durchstich:"działający szkielet",
|
||||
st_fertig:"gotowe", st_prod:"w produkcji", st_highrisk:"wysokie ryzyko", st_verworfen:"odrzucone",
|
||||
@@ -2460,6 +2479,8 @@ const I18N = {
|
||||
unknownDepWarn:"Строка {line}: зависимость #{id} — узла с таким ID нет.",
|
||||
unknownDescWarn:"Строка {line}: описание для #{id} — узла с таким ID нет.",
|
||||
descStrayWarn:"Строка {line}: строка описания без привязки — перед ней нет узла или блока #id.",
|
||||
sizeConflictWarn:"Строка {line}: подзадачи вместе превышают указанный размер ({size}) — даже при самой оптимистичной оценке.",
|
||||
sizeConflictTooltip:"Подзадачи вместе превышают указанный размер",
|
||||
cheapApproxWarn:"Слишком много связанных групп альтернатив для точного поиска — самый дешёвый путь оценён жадно (локальный выбор в каждой группе).",
|
||||
st_idee:"идея", st_geplant:"запланировано", st_arbeit:"в работе", st_durchstich:"сквозной прототип",
|
||||
st_fertig:"готово", st_prod:"в эксплуатации", st_highrisk:"высокий риск", st_verworfen:"отклонено",
|
||||
@@ -2561,6 +2582,8 @@ const I18N = {
|
||||
unknownDepWarn:"पंक्ति {line}: निर्भरता #{id} — इस आईडी वाला कोई नोड नहीं है।",
|
||||
unknownDescWarn:"पंक्ति {line}: #{id} के लिए विवरण — इस आईडी वाला कोई नोड नहीं है।",
|
||||
descStrayWarn:"पंक्ति {line}: विवरण पंक्ति बिना संदर्भ — इससे पहले कोई नोड या #id ब्लॉक नहीं है।",
|
||||
sizeConflictWarn:"पंक्ति {line}: उप-पैकेज मिलकर दिए गए आकार ({size}) से बड़े हैं — सबसे आशावादी आकलन में भी।",
|
||||
sizeConflictTooltip:"उप-पैकेज मिलकर दिए गए आकार से बड़े हैं",
|
||||
cheapApproxWarn:"सटीक खोज के लिए बहुत सारे युग्मित विकल्प-समूह — सबसे सस्ता पथ लालची अनुमान है (प्रति समूह स्थानीय चयन)।",
|
||||
st_idee:"विचार", st_geplant:"नियोजित", st_arbeit:"प्रगति पर", st_durchstich:"कार्यशील ढाँचा",
|
||||
st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_highrisk:"उच्च जोखिम", st_verworfen:"अस्वीकृत",
|
||||
@@ -2662,6 +2685,8 @@ const I18N = {
|
||||
unknownDepWarn:"第 {line} 行:依赖 #{id}——没有节点使用此 ID。",
|
||||
unknownDescWarn:"第 {line} 行:#{id} 的描述——没有节点使用此 ID。",
|
||||
descStrayWarn:"第 {line} 行:描述行没有归属——前面缺少节点或 #id 块。",
|
||||
sizeConflictWarn:"第 {line} 行:子项合计超出所标注的尺寸({size})——即使按最乐观的估算也是如此。",
|
||||
sizeConflictTooltip:"子项合计超出所标注的尺寸",
|
||||
cheapApproxWarn:"耦合的备选组过多,无法精确搜索——最便宜路径为贪心估计(每组就地选择)。",
|
||||
st_idee:"想法", st_geplant:"已计划", st_arbeit:"进行中", st_durchstich:"可运行骨架",
|
||||
st_fertig:"已完成", st_prod:"已上线", st_highrisk:"高风险", st_verworfen:"已放弃",
|
||||
@@ -2763,6 +2788,8 @@ const I18N = {
|
||||
unknownDepWarn:"{line} 行目:依存 #{id} — この ID を持つノードはありません。",
|
||||
unknownDescWarn:"{line} 行目:#{id} の説明 — この ID を持つノードはありません。",
|
||||
descStrayWarn:"{line} 行目:説明行の帰属先がありません — 直前にノードまたは #id ブロックが必要です。",
|
||||
sizeConflictWarn:"{line} 行目:サブパッケージの合計が指定サイズ({size})を超えています — 最も楽観的な見積もりでも。",
|
||||
sizeConflictTooltip:"サブパッケージの合計が指定サイズを超えています",
|
||||
cheapApproxWarn:"結合された選択肢グループが多すぎるため厳密探索は不可 — 最安パスは貪欲法による推定です(グループごとに局所選択)。",
|
||||
st_idee:"アイデア", st_geplant:"計画済み", st_arbeit:"作業中", st_durchstich:"ウォーキングスケルトン",
|
||||
st_fertig:"完了", st_prod:"本番稼働", st_highrisk:"高リスク", st_verworfen:"破棄",
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
/* T-Shirt-Größen (SPEC §5), aufsteigend geordnet. */
|
||||
export const SIZE_RANK = { XS: 0, S: 1, M: 2, L: 3, XL: 4, XXL: 5 };
|
||||
|
||||
/* Größen als BEREICHE für die Konflikt-Prüfung (SPEC §5/D62): Untergrenze
|
||||
2^Rang, Obergrenze die Untergrenze der nächsten Größe — XXL ist nach oben
|
||||
offen, ein XXL-Element warnt also nie. Die Skala bleibt ansonsten ordinal
|
||||
(D46: „S+S ≠ M" gilt weiter für alles außer dieser einen Prüfung). */
|
||||
export const sizeMin = size => 2 ** SIZE_RANK[size];
|
||||
export const sizeMax = size => size === 'XXL' ? Infinity : 2 ** (SIZE_RANK[size] + 1);
|
||||
|
||||
/* Status-Vokabular (SPEC §4): Checkbox-Code -> {code, key, name}.
|
||||
`name` ist der deutsche Anzeigename (Quellsprache); `code` das kanonische
|
||||
Box-Zeichen (für die Diskrepanz-Marke des effektiven Status, D39). */
|
||||
@@ -384,5 +391,33 @@ export function parse(text){
|
||||
kids.forEach(checkXor);
|
||||
})(virtualRoot);
|
||||
|
||||
/* Größen-Konflikt (SPEC §5/D62): Die angegebene Größe muss zu den direkten
|
||||
Kindern passen. Jede Größe zählt als BEREICH (sizeMin/sizeMax); Konflikt
|
||||
erst, wenn selbst die günstigste Lesart der Kinder die großzügigste des
|
||||
Elternknotens erreicht — gemeldet wird nur, was unter jeder Lesart falsch
|
||||
ist. Es zählen nur Kinder MIT Größe (fehlende Größe ist keine Aussage —
|
||||
anders als bei den Pfadkosten wird hier kein M angenommen, D44-Linie),
|
||||
ohne verworfene und ohne optionale; in einer disjunktiven Gruppe wird nur
|
||||
eine Alternative realisiert, dort zählt die kleinste. Nichts wird
|
||||
korrigiert — Warnung an der Elternzeile plus Flag für das Badge. */
|
||||
(function checkSizes(node){
|
||||
const kids = node.children;
|
||||
if(node.size && kids.length && sizeMax(node.size) !== Infinity){
|
||||
const counted = kids.filter(k =>
|
||||
k.size && !k.optional && !(k.status && k.status.key === 'verworfen'));
|
||||
if(counted.length){
|
||||
const mins = counted.map(k => sizeMin(k.size));
|
||||
const need = kids[0].type !== 'and'
|
||||
? Math.min(...mins)
|
||||
: mins.reduce((a, b) => a + b, 0);
|
||||
if(need >= sizeMax(node.size)){
|
||||
node.sizeConflict = true;
|
||||
warnings.push({type:'sizeConflict', line: node.line, size: node.size});
|
||||
}
|
||||
}
|
||||
}
|
||||
kids.forEach(checkSizes);
|
||||
})(virtualRoot);
|
||||
|
||||
return {roots: virtualRoot.children, warnings};
|
||||
}
|
||||
|
||||
@@ -72,6 +72,9 @@ function nodeAria(n, opts, fold){
|
||||
if(effKey) parts.push(t('a11yEffective', {status: t('st_' + effKey)}));
|
||||
if(n.size) parts.push(t('a11ySize', {size: n.size}));
|
||||
else if(cheapPath && !isDone(n)) parts.push(t('a11ySizeImplicit'));
|
||||
/* Größen-Konflikt (SPEC §5/D62): die Warnfärbung des Badges kommt beim
|
||||
Screenreader sonst nicht an. */
|
||||
if(n.sizeConflict) parts.push(t('sizeConflictTooltip'));
|
||||
if(n.tags && n.tags.length) parts.push(t('a11yTags', {names: n.tags.join(', ')}));
|
||||
/* Knoten-ID und Abhängigkeiten (SPEC §1, D36/D37): keine eigene Darstellung
|
||||
im Diagramm — sichtbar nur im Tooltip und hier. */
|
||||
@@ -138,6 +141,9 @@ function nodeHtml(n, extra, opts, fold){
|
||||
eigenen `title` am Badge — der erschiene neben dem Fenster
|
||||
ein zweites Mal. Sie gehört ohnehin zu den Kurz-Fakten. */
|
||||
(!n.size && cheapPath && !isDone(n)) ? t('implicitSizeTooltip') : '',
|
||||
/* Größen-Konflikt (SPEC §5/D62): das warnfarbene Badge braucht
|
||||
seine Begründung dort, wo man nachsieht. */
|
||||
n.sizeConflict ? t('sizeConflictTooltip') : '',
|
||||
t('jumpHint')]
|
||||
.filter(Boolean).join(' · ');
|
||||
const tip = n.desc && facts ? n.desc + '\n\n' + TIP_RULE + '\n' + facts
|
||||
@@ -154,8 +160,10 @@ function nodeHtml(n, extra, opts, fold){
|
||||
erledigten Knoten wird keine getroffen — er kostet nichts mehr (D46) —,
|
||||
dort bleibt es deshalb weg. */
|
||||
const implicitSize = !n.size && cheapPath && !isDone(n);
|
||||
/* Größen-Konflikt (SPEC §5/D62): das Badge wechselt auf die Warnfarbe —
|
||||
die Größe selbst bleibt stehen, korrigiert wird nichts. */
|
||||
const sizeBadge = n.size
|
||||
? `<span class="size" aria-hidden="true">${n.size}</span>`
|
||||
? `<span class="size${n.sizeConflict ? ' conflict' : ''}" aria-hidden="true">${n.size}</span>`
|
||||
: (implicitSize ? `<span class="size implicit" aria-hidden="true">M</span>` : '');
|
||||
/* High-Risk: Warndreieck (⚠, Textpräsentation via VS15) an der oberen linken
|
||||
Ecke. aria-hidden — die Information steckt bereits im Status des aria-label. */
|
||||
|
||||
@@ -910,6 +910,10 @@
|
||||
.cheap-on .node:not(.cheap).done{opacity:1;filter:none}
|
||||
/* implizit als M angenommene Größe: invertiertes Badge (weiß statt petrol) */
|
||||
.size.implicit{background:var(--card);color:var(--or);border-color:var(--or)}
|
||||
/* Größen-Konflikt (SPEC §5/D62): die Teilpakete übersteigen zusammen die
|
||||
angegebene Größe — das Badge wechselt auf die Warnfarbe, der weiße
|
||||
Trennrand zur Knotenkante bleibt. */
|
||||
.size.conflict{background:var(--warn)}
|
||||
/* Günstigster-Pfad-Linie: gestrichelt, geschwungen, petrol — fädelt durch
|
||||
die Endknoten (Blätter) des Pfads. Zwei Overlays in #out (erben dessen
|
||||
zoom): die kräftige Linie liegt HINTER den Knoten (nur in den Lücken
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
Beschreibungsteil ohne #id-Block; meldet
|
||||
auch die von einem versehentlichen
|
||||
`---` verschluckten Knotenzeilen
|
||||
- sizeConflict { line, size } — Teilpakete übersteigen zusammen die
|
||||
angegebene Größe des Elternknotens, selbst
|
||||
in der günstigsten Bereichs-Lesart
|
||||
(SPEC §5/D62); Zeile = Elternknoten
|
||||
- cheapApprox { } — günstigster Pfad nur gierig geschätzt:
|
||||
zu viele gekoppelte Gruppen für die
|
||||
exakte Suche (D42); zeilenlos
|
||||
@@ -72,6 +76,8 @@ function build(w, t, esc){
|
||||
return t('unknownDescWarn', {line: w.line, id: esc(w.id)});
|
||||
case 'descStray':
|
||||
return t('descStrayWarn', {line: w.line});
|
||||
case 'sizeConflict':
|
||||
return t('sizeConflictWarn', {line: w.line, size: esc(w.size)});
|
||||
case 'cheapApprox':
|
||||
return t('cheapApproxWarn');
|
||||
case 'sourceLoad':
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parse, sizeMin, sizeMax } from '../src/parser.js';
|
||||
|
||||
/* Größen-Konflikt (SPEC §5/D62): Die angegebene Größe muss zu den direkten
|
||||
Kindern passen. Jede Größe zählt als Bereich (Untergrenze 2^Rang, Obergrenze
|
||||
die nächste Untergrenze, XXL offen); Konflikt erst, wenn selbst die
|
||||
günstigste Lesart der Kinder die großzügigste des Elternknotens erreicht.
|
||||
Nichts wird automatisch korrigiert. */
|
||||
|
||||
const conflicts = txt => parse(txt).warnings.filter(w => w.type === 'sizeConflict');
|
||||
const node = (txt, label) => {
|
||||
const find = ns => {
|
||||
for(const n of ns){
|
||||
if(n.label === label) return n;
|
||||
const hit = find(n.children);
|
||||
if(hit) return hit;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return find(parse(txt).roots);
|
||||
};
|
||||
|
||||
describe('Bereichs-Skala', () => {
|
||||
it('Untergrenzen verdoppeln sich, Obergrenze ist die nächste Untergrenze', () => {
|
||||
expect(['XS','S','M','L','XL'].map(sizeMin)).toEqual([1,2,4,8,16]);
|
||||
expect(['XS','S','M','L','XL'].map(sizeMax)).toEqual([2,4,8,16,32]);
|
||||
});
|
||||
it('XXL ist nach oben offen', () => {
|
||||
expect(sizeMin('XXL')).toBe(32);
|
||||
expect(sizeMax('XXL')).toBe(Infinity);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Konflikt-Regel (konjunktiv: Summe der Untergrenzen)', () => {
|
||||
it('vier S unter einem M sind ein Konflikt (8 >= 8), drei nicht', () => {
|
||||
const vier = 'A (M)\n - B (S)\n - C (S)\n - D (S)\n - E (S)';
|
||||
expect(conflicts(vier)).toEqual([{type:'sizeConflict', line:1, size:'M'}]);
|
||||
expect(node(vier, 'A').sizeConflict).toBe(true);
|
||||
expect(conflicts('A (M)\n - B (S)\n - C (S)\n - D (S)')).toEqual([]);
|
||||
});
|
||||
it('ein einzelnes Kind derselben Größe passt, ein größeres nie', () => {
|
||||
expect(conflicts('A (M)\n - B (M)')).toEqual([]);
|
||||
expect(conflicts('A (M)\n - B (L)'))
|
||||
.toEqual([{type:'sizeConflict', line:1, size:'M'}]);
|
||||
});
|
||||
it('das kanonische Beispiel bleibt sauber: XL = XL-Kind + 2×M', () => {
|
||||
expect(conflicts('A (XL)\n - B (M)\n - C (XL)\n - D (M)')).toEqual([]);
|
||||
});
|
||||
it('zwei M unter einem M sind ein Konflikt (4+4 >= 8)', () => {
|
||||
expect(conflicts('A (M)\n - B (M)\n - C (M)').length).toBe(1);
|
||||
});
|
||||
it('ein XXL-Elternknoten warnt nie', () => {
|
||||
const txt = 'A (XXL)\n' + Array.from({length: 40}, (_, i) => ` - K${i} (XL)`).join('\n');
|
||||
expect(conflicts(txt)).toEqual([]);
|
||||
});
|
||||
it('geprüft wird je Ebene, die Warnung zeigt auf die Elternzeile', () => {
|
||||
const txt = 'A (XXL)\n - B (S)\n - C (S)\n - D (S)';
|
||||
expect(conflicts(txt)).toEqual([{type:'sizeConflict', line:2, size:'S'}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Was nicht zählt', () => {
|
||||
it('ein Elternknoten ohne Größe macht keine Aussage', () => {
|
||||
expect(conflicts('A\n - B (XXL)\n - C (XXL)')).toEqual([]);
|
||||
});
|
||||
it('Kinder ohne Größe zählen nicht — hier wird kein M angenommen', () => {
|
||||
expect(conflicts('A (XS)\n - B\n - C\n - D')).toEqual([]);
|
||||
/* die größenlosen bleiben auch neben gezählten außen vor */
|
||||
expect(conflicts('A (M)\n - B (S)\n - C\n - D')).toEqual([]);
|
||||
});
|
||||
it('verworfene Kinder zählen nicht', () => {
|
||||
expect(conflicts('A (M)\n - [-] B (XL)\n - C (S)')).toEqual([]);
|
||||
});
|
||||
it('optionale Kinder (`+`) zählen nicht', () => {
|
||||
expect(conflicts('A (M)\n - B (S)\n + C (XL)')).toEqual([]);
|
||||
});
|
||||
it('die Gegenrichtung warnt nicht: Eltern größer als die Kindersumme', () => {
|
||||
expect(conflicts('A (XXL)\n - B (XS)')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Disjunktive Gruppen: nur die kleinste Alternative zählt', () => {
|
||||
it('| mit einer passenden Alternative ist kein Konflikt', () => {
|
||||
expect(conflicts('A (M)\n | B (L)\n | C (S)')).toEqual([]);
|
||||
});
|
||||
it('| warnt erst, wenn selbst die kleinste Alternative sprengt', () => {
|
||||
expect(conflicts('A (M)\n | B (L)\n | C (L)').length).toBe(1);
|
||||
});
|
||||
it('= verhält sich wie |', () => {
|
||||
expect(conflicts('A (M)\n = B (L)\n = C (S)')).toEqual([]);
|
||||
expect(conflicts('A (M)\n = B (L)\n = C (L)').length).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user