lean-path: Angefangenes liegt auf dem Pfad (SPEC §9, D61)
Eine Zugabe (`+`) fällt aus dem günstigsten Pfad — außer sie ist realisiert und noch nicht erledigt (`[~]`/`[/]`). Angefangene Arbeit ist die offene Front, und seit D46 zeigt der Pfad genau die; bisher führte der Von-Station-zu-Station-Knopf nie dorthin, wo gearbeitet wird. Erledigte Zugaben bleiben draußen: dort ist nichts mehr zu tun, und was darunter offen blieb, ist mit ihnen zusammen entbehrlich (§3). Dabei die fehlende Hälfte von D46 gefunden: SPEC §9 sagt seit jeher „eine bereits realisierte Alternative gewinnt", umgesetzt war das aber nur über die Kosten — und die sind allein bei `[x]`/`[^]` null. Ein `[~]`-Zweig verlor damit gegen eine unangetastete billigere, obwohl die XOR-Regel gerade ihn als die realisierte Alternative ausweist. `chosenPool()` schränkt die Wahlmenge auf die realisierten ein; unter mehreren entscheiden wieder die Kosten. Nebengewinn: eine entschiedene Gruppe ist keine freie Variable mehr und verkleinert die Suche (D42). 10 neue Tests; Gegenprobe geführt (jede Regel einzeln herausgenommen lässt genau die nach ihr benannten Zusicherungen fallen). Der mitgelieferte Plan ändert sich nicht — er hat keine angefangene Zugabe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3018db9f61
commit
36b78efa53
@@ -19,6 +19,8 @@ reverse.
|
|||||||
|
|
||||||
## 2026-08-24
|
## 2026-08-24
|
||||||
|
|
||||||
|
- An optional node (`+`) joins the cheapest path while it is being worked on — started work is the open front
|
||||||
|
- Fix: a started alternative lost its `|`/`=` group to an untouched cheaper one, although the choice was already made
|
||||||
- A node written as an id alone (`- #US-123`) is now titled by that id, instead of being ignored
|
- A node written as an id alone (`- #US-123`) is now titled by that id, instead of being ignored
|
||||||
- A line ending in a space and `\` continues on the next line, so a long node no longer has to fit into one
|
- A line ending in a space and `\` continues on the next line, so a long node no longer has to fit into one
|
||||||
- A star in the header opens What's new: the changes of the last few days, and a link per day that shows the nodes it touched in the diagram
|
- A star in the header opens What's new: the changes of the last few days, and a link per day that shows the nodes it touched in the diagram
|
||||||
|
|||||||
@@ -4319,3 +4319,100 @@ Knoten.
|
|||||||
`#`-Umschalter bekommt er **keine** `nid`-Spanne, der Nachbar `#auth: Backend`
|
`#`-Umschalter bekommt er **keine** `nid`-Spanne, der Nachbar `#auth: Backend`
|
||||||
schon. 337 Tests, davon 6 neue; der eine alte, der die frühere Regel festhielt,
|
schon. 337 Tests, davon 6 neue; der eine alte, der die frühere Regel festhielt,
|
||||||
ist umgeschrieben und benennt jetzt diese.
|
ist umgeschrieben und benennt jetzt diese.
|
||||||
|
|
||||||
|
## D61 — Angefangenes liegt auf dem Pfad: Zugaben und Alternativen
|
||||||
|
Gemeldet an einem kleinen Baum:
|
||||||
|
|
||||||
|
```
|
||||||
|
- [ ] #fe
|
||||||
|
+ [~] #fe.rel: Relations bearbeitbar (S)
|
||||||
|
+ [?] #fe.more: weitere Features …
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet war, dass der Pfad durch `#fe.rel` läuft — „auch wenn es optional
|
||||||
|
ist, weil das ja nun schon begonnen wurde". Er lief stattdessen durch `#fe`,
|
||||||
|
und die einzige Station war der Elternknoten.
|
||||||
|
|
||||||
|
**Die Regel dahinter stand seit D29 ohne Ausnahme da:** Optionale Knoten sind
|
||||||
|
*nie* nötig. Sie ist aus einem echten Fehler entstanden — der Pfad rechnete
|
||||||
|
jede Zugabe ins Minimum und **überschätzte** sich systematisch. Der umgekehrte
|
||||||
|
Fehler war dabei nie bedacht: angefangene Arbeit zu **unterschlagen**. Seit
|
||||||
|
D46 beantwortet der Pfad ohnehin nicht mehr „was hätte der Plan von vorn
|
||||||
|
gekostet?", sondern „was ist als Nächstes dran?" — und `[~]` ist das
|
||||||
|
Vorderste, was es gibt. Praktisch heißt das, dass der
|
||||||
|
Von-Station-zu-Station-Knopf (D47) einen nie dorthin führt, wo gerade
|
||||||
|
tatsächlich gearbeitet wird.
|
||||||
|
|
||||||
|
`+` ist eine Aussage über den **Plan** („entbehrlich"), der Status eine über
|
||||||
|
die **Tatsachen** („daran wird gearbeitet"). Die beiden Achsen bleiben
|
||||||
|
unabhängig (§3) — der Pfad ist ein Drittes und darf beide lesen.
|
||||||
|
|
||||||
|
**Entschieden: Eine Zugabe liegt auf dem Pfad, sobald sie realisiert (§3),
|
||||||
|
aber noch nicht erledigt (D46) ist** — also bei `[~]` und `[/]`.
|
||||||
|
|
||||||
|
**Die Formulierung ist der eigentliche Fund.** Mein erster Vorschlag war die
|
||||||
|
volle Schwelle „realisiert" (`[~] [/] [x] [^]`), begründet damit, dass §3 das
|
||||||
|
Wort schon führt und keine dritte Schwelle dazukommt. Der Nutzer hat
|
||||||
|
widersprochen: *„Eigentlich sollen fertige oder gar deployed Knoten gar nicht
|
||||||
|
auf den Lean-Path, da gibt es ja nichts mehr zu tun."* Das ist richtig, und
|
||||||
|
mein Argument gegen die engere Fassung fällt weg, sobald man sie aus den
|
||||||
|
**vorhandenen** Begriffen zusammensetzt: *realisiert, aber nicht erledigt*.
|
||||||
|
`isStarted = isRealized && !isDone` — kein neues Vokabular.
|
||||||
|
|
||||||
|
**Was dabei zu klären war: „auf dem Pfad" ist nicht „Station".** Seit D46
|
||||||
|
bekommt ein erledigter Knoten auf dem Pfad **keinen** Stationspunkt, **keine**
|
||||||
|
Pfadlinie und geht mit **0** in die Kosten; übrig bleibt allein, dass er nicht
|
||||||
|
zurücktritt — und das tut er per D46-Nachtrag ohnehin nie. Für den fertigen
|
||||||
|
Knoten **selbst** war der Unterschied zwischen beiden Schwellen also exakt
|
||||||
|
null. Er lag einzig darin, ob der Pfad **in eine fertige Zugabe hineinschaut**
|
||||||
|
und dort liegen gebliebene offene Kinder als Station zeigt. Auch das entfällt
|
||||||
|
jetzt, und zwar mit einem eigenen Argument aus §3: Wer unter einem `+`-Knoten
|
||||||
|
hängt, ist mit ihm zusammen entbehrlich — der Autor hat `[x]` geschrieben, ein
|
||||||
|
offener Rest darunter ist Buchhaltung, keine offene Front.
|
||||||
|
|
||||||
|
**Ehrlich zu benennen ist der Einwand gegen die ganze Regel:** Streng „am
|
||||||
|
günstigsten" wäre es, die angefangene Zugabe **abzubrechen** — Restkosten
|
||||||
|
gespart. Der Pfad zeigt seit D46 aber die offene Front, nicht das theoretische
|
||||||
|
Optimum. Und der Rückweg steht in der Notation schon: Wer die Zugabe wirklich
|
||||||
|
fallen lässt, schreibt `[-]`, und Verworfenes zählt nie.
|
||||||
|
|
||||||
|
**Dabei gefunden: dieselbe Lücke bei Alternativen — und die SPEC hatte recht,
|
||||||
|
der Code nicht.** §9 sagt seit D46 wörtlich: „Eine bereits realisierte
|
||||||
|
Alternative gewinnt, auch wenn eine unangetastete nominell billiger wäre — die
|
||||||
|
Wahl ist getroffen und bezahlt." Umgesetzt war das aber nur **über die
|
||||||
|
Kosten**, und die sind allein bei `[x]`/`[^]` null. Nachgemessen an
|
||||||
|
`= [~] A (L)` / `= [ ] B (S)`: Der Pfad wählte **B** und blasste das
|
||||||
|
angefangene A aus — das Bild widersprach damit der XOR-Regel des Plans, die
|
||||||
|
gerade sagt, dass A die realisierte Alternative ist. Also kein neues Feature,
|
||||||
|
sondern die fehlende Hälfte der Umsetzung: `chosenPool(kids)` schränkt die
|
||||||
|
Wahlmenge auf die realisierten Alternativen ein, sobald es welche gibt; die
|
||||||
|
Kostenregel (kleinste rekursive Kosten, Gleichstand ⇒ erste) gilt darin
|
||||||
|
unverändert.
|
||||||
|
|
||||||
|
**Mehrere realisierte Alternativen** sind in einer `=`-Gruppe schon per
|
||||||
|
`xorConflict` gemeldet, in einer `|`-Gruppe („mindestens eine") aber zulässig.
|
||||||
|
Dort entscheiden unter ihnen wieder die Kosten. Erwogen und **aufgeschoben**:
|
||||||
|
alle realisierten gemeinsam auf den Pfad zu nehmen. Das wäre ein größerer
|
||||||
|
Eingriff — eine any-of-Gruppe trüge dann nicht mehr genau eine Alternative —,
|
||||||
|
und die gewählte Lesart ist für sich verteidigbar: `|` erlaubt das Fallenlassen,
|
||||||
|
also ist „die billigere der begonnenen fertigstellen" ein gültiger günstigster
|
||||||
|
Weg.
|
||||||
|
|
||||||
|
**Nebengewinn bei der Suche (D42):** Eine Gruppe mit genau einer realisierten
|
||||||
|
Alternative ist entschieden und damit **keine freie Variable** mehr — sie
|
||||||
|
koppelt nicht und geht nicht in den Odometer ein. Die erschöpfende Suche wird
|
||||||
|
dadurch kleiner, nie größer.
|
||||||
|
|
||||||
|
**Nachgemessen.** Der gemeldete Baum: `#fe.rel` ist jetzt die Station, `#fe`
|
||||||
|
liegt auf dem Pfad ohne Punkt, `#fe.more` bleibt blass. Eine fertige Zugabe
|
||||||
|
mit offenem Kind bleibt samt Kind draußen. `= [~] A (L)` schlägt
|
||||||
|
`= [ ] B (S)`. Der **mitgelieferte Plan ändert sich nicht** (131 Pfadknoten,
|
||||||
|
27 Stationen, vorher wie nachher) — er hat keine angefangene Zugabe, und seine
|
||||||
|
einzige Gruppe mit realisierten Alternativen trägt zwei `[^]`, die schon
|
||||||
|
vorher beide 0 kosteten.
|
||||||
|
|
||||||
|
346 Tests, davon 10 neue. Gegenprobe: Nimmt man die Zugaben-Ausnahme wieder
|
||||||
|
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.
|
||||||
|
|||||||
+15
-4
@@ -545,10 +545,21 @@ werden die für die günstigste Realisierung **nötigen** Knoten:
|
|||||||
- **any of:** nur die **günstigste** Alternative ist nötig. „Günstig" =
|
- **any of:** nur die **günstigste** Alternative ist nötig. „Günstig" =
|
||||||
kleinste rekursive Kosten (eigene T-Shirt-Größe plus — je Gate — Summe bzw.
|
kleinste rekursive Kosten (eigene T-Shirt-Größe plus — je Gate — Summe bzw.
|
||||||
Minimum der Kinder). Bei Gleichstand gewinnt die **erste** Alternative.
|
Minimum der Kinder). Bei Gleichstand gewinnt die **erste** Alternative.
|
||||||
- **Optionale Knoten (`+`, §3) sind nie nötig** — sie zählen weder zu den
|
**Ist in der Gruppe etwas realisiert** (§3: `[~]`, `[/]`, `[x]`, `[^]`), ist
|
||||||
Kosten ihres Elternknotens noch liegen sie auf dem Pfad, und der Teilbaum
|
die Wahl damit getroffen — gewählt wird nur noch unter den realisierten
|
||||||
unter ihnen ebenso wenig. Genau dafür gibt es das Zeichen: Ohne `+` rechnet
|
Alternativen, auch wenn eine unangetastete nominell billiger wäre. Sind es
|
||||||
der günstigste Pfad jede Zugabe ins Minimum ein und überschätzt es.
|
mehrere (in einer `=`-Gruppe schon per `xorConflict` gemeldet, in einer
|
||||||
|
`|`-Gruppe zulässig), entscheidet unter ihnen wieder die Kostenregel.
|
||||||
|
- **Optionale Knoten (`+`, §3) sind nur nötig, solange an ihnen gearbeitet
|
||||||
|
wird** — also wenn sie realisiert (§3), aber noch nicht erledigt sind:
|
||||||
|
`[~]` und `[/]`. Sonst zählen sie weder zu den Kosten ihres Elternknotens
|
||||||
|
noch liegen sie auf dem Pfad, und der Teilbaum unter ihnen ebenso wenig.
|
||||||
|
Genau dafür gibt es das Zeichen: Ohne `+` rechnet der günstigste Pfad jede
|
||||||
|
Zugabe ins Minimum ein und überschätzt es. Die Ausnahme hält den umgekehrten
|
||||||
|
Fehler heraus — eine angefangene Zugabe ist offene Arbeit, und der Pfad
|
||||||
|
zeigt die offene Front. **Erledigte Zugaben bleiben draußen:** Dort ist
|
||||||
|
nichts mehr zu tun, und was darunter offen blieb, ist mit ihnen zusammen
|
||||||
|
entbehrlich (§3). Siehe D61.
|
||||||
- Verworfene Knoten zählen nie mit (unabhängig vom „verworfene einblenden"-
|
- Verworfene Knoten zählen nie mit (unabhängig vom „verworfene einblenden"-
|
||||||
Filter).
|
Filter).
|
||||||
- **Fehlende Größe wird als `M` gewertet** (nur für diese Kostenschätzung; die
|
- **Fehlende Größe wird als `M` gewertet** (nur für diese Kostenschätzung; die
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
- [^] #ed.path: Cheapest path (M)
|
- [^] #ed.path: Cheapest path (M)
|
||||||
- [^] #ed.path.cost: Cost model, missing size counts as M (S)
|
- [^] #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.front: Only the open front: what is done costs nothing (S) %% see D46
|
||||||
|
- [x] #ed.path.started: Started work is on the path, extras included (S) %% see D61
|
||||||
- [^] #ed.path.line: Metro-map line through the leaves (S)
|
- [^] #ed.path.line: Metro-map line through the leaves (S)
|
||||||
- [^] #ed.path.step: Step from station to station, one button (S) %% see D47
|
- [^] #ed.path.step: Step from station to station, one button (S) %% see D47
|
||||||
- [^] #ed.closure: Count the whole dependency closure (M)
|
- [^] #ed.closure: Count the whole dependency closure (M)
|
||||||
@@ -385,6 +386,11 @@
|
|||||||
what the plan would have cost from scratch. An alternative already built
|
what the plan would have cost from scratch. An alternative already built
|
||||||
therefore wins its group even against a cheaper untouched one.
|
therefore wins its group even against a cheaper untouched one.
|
||||||
|
|
||||||
|
#ed.path.started
|
||||||
|
An extra (`+`) joins the path while it is being worked on, and a started
|
||||||
|
alternative wins its group. Optionality says the plan can do without it;
|
||||||
|
the status says work has begun, and the front is made of exactly that.
|
||||||
|
|
||||||
#ed.path.line
|
#ed.path.line
|
||||||
A dashed curve threads through the open leaves of the path, with a pale
|
A dashed curve threads through the open leaves of the path, with a pale
|
||||||
station dot at each. Borrowed from metro maps: the dot is large and faint so
|
station dot at each. Borrowed from metro maps: the dot is large and faint so
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ One node per line. Everything except the label is optional.
|
|||||||
`[~]`, `[/]`, `[x]` or `[^]`); each additional realized one warns
|
`[~]`, `[/]`, `[x]` or `[^]`); each additional realized one warns
|
||||||
(`xorConflict`).
|
(`xorConflict`).
|
||||||
- Optionality (`+`) is orthogonal to status: an optional node can be long
|
- Optionality (`+`) is orthogonal to status: an optional node can be long
|
||||||
`[^]`. Optional nodes never count toward the cheapest path.
|
`[^]`. An optional node counts toward the cheapest path **only while it is
|
||||||
|
being worked on** — `[~]` or `[/]`; untouched and finished ones stay out.
|
||||||
|
|
||||||
### Fold mark (immediately before the label, i.e. after the status box)
|
### Fold mark (immediately before the label, i.e. after the status box)
|
||||||
|
|
||||||
@@ -96,10 +97,11 @@ One node per line. Everything except the label is optional.
|
|||||||
- **`[x]` and `[^]` cost nothing any more.** The cheapest path prices the
|
- **`[x]` and `[^]` cost nothing any more.** The cheapest path prices the
|
||||||
work that is *left*, so a done node adds zero regardless of its size and
|
work that is *left*, so a done node adds zero regardless of its size and
|
||||||
carries no station on the path line; started work (`[~]`, `[/]`) still
|
carries no station on the path line; started work (`[~]`, `[/]`) still
|
||||||
counts in full. A realized alternative therefore wins its `|`/`=` group
|
counts in full. **A realized alternative wins its `|`/`=` group** even when
|
||||||
even when a cheaper unstarted one sits next to it. The *intrinsic* status
|
a cheaper unstarted one sits next to it — the choice has been made; among
|
||||||
decides — dependencies may hold a node back effectively, but the work on
|
several realized ones cost decides again. The *intrinsic* status decides —
|
||||||
it is paid for.
|
dependencies may hold a node back effectively, but the work on it is paid
|
||||||
|
for.
|
||||||
|
|
||||||
### Size (effort)
|
### Size (effort)
|
||||||
|
|
||||||
|
|||||||
+36
-12
@@ -40,14 +40,18 @@ export function visibleChildren(n, showDiscarded){
|
|||||||
any-of und XOR ⇒ nur die günstigste Alternative. „Günstig" = kleinste rekursive
|
any-of und XOR ⇒ nur die günstigste Alternative. „Günstig" = kleinste rekursive
|
||||||
Kosten (eigene Größe + Kinder; any-of das Minimum). Verworfene zählen nie
|
Kosten (eigene Größe + Kinder; any-of das Minimum). Verworfene zählen nie
|
||||||
mit (unabhängig vom Einblenden-Toggle). Gleichstand ⇒ erste. Fehlende
|
mit (unabhängig vom Einblenden-Toggle). Gleichstand ⇒ erste. Fehlende
|
||||||
Größe = M.
|
Größe = M. Ist in einer disjunktiven Gruppe etwas realisiert, wird nur noch
|
||||||
|
unter den realisierten gewählt (`chosenPool`, D61).
|
||||||
Optionale Kinder (`+`, SPEC §3/D29) fallen hier ebenfalls heraus — sie sind
|
Optionale Kinder (`+`, SPEC §3/D29) fallen hier ebenfalls heraus — sie sind
|
||||||
per Definition entbehrlich, also weder Kostenanteil noch Pfadknoten. Da beide
|
per Definition entbehrlich, also weder Kostenanteil noch Pfadknoten. Da alle
|
||||||
Nutzer (`cheapestCost`, `markCheapest`) über diese Funktion gehen, gilt das
|
Nutzer über diese Funktion gehen, gilt das samt Teilbaum.
|
||||||
samt Teilbaum. */
|
AUSNAHME (D61): Wird an der Zugabe gerade gearbeitet (`isStarted`), liegt
|
||||||
|
sie auf dem Pfad — angefangene Arbeit IST die offene Front. Erledigte
|
||||||
|
Zugaben bleiben draußen: Dort ist nichts mehr zu tun, und was darunter
|
||||||
|
offen blieb, ist mit ihnen zusammen entbehrlich (§3). */
|
||||||
export function pathChildren(n){
|
export function pathChildren(n){
|
||||||
return n.children.filter(k =>
|
return n.children.filter(k =>
|
||||||
!k.optional && (!k.status || k.status.key !== 'verworfen'));
|
(!k.optional || isStarted(k)) && (!k.status || k.status.key !== 'verworfen'));
|
||||||
}
|
}
|
||||||
/* ---------- Erledigt: was nichts mehr kostet (SPEC §9, D46) ----------
|
/* ---------- Erledigt: was nichts mehr kostet (SPEC §9, D46) ----------
|
||||||
`[x]` fertig und `[^]` in Produktion. Die Beförderung auf Prod ist keine
|
`[x]` fertig und `[^]` in Produktion. Die Beförderung auf Prod ist keine
|
||||||
@@ -60,13 +64,29 @@ export function pathChildren(n){
|
|||||||
export function isDone(n){
|
export function isDone(n){
|
||||||
return !!n.status && (n.status.key === 'fertig' || n.status.key === 'prod');
|
return !!n.status && (n.status.key === 'fertig' || n.status.key === 'prod');
|
||||||
}
|
}
|
||||||
|
/* „realisiert" (SPEC §3, D35): Kosten investiert oder mehr. Trägt die
|
||||||
|
XOR-Regel und — seit D61 — die Wahl in Alternativgruppen. */
|
||||||
|
export function isRealized(n){
|
||||||
|
return !!n.status && ['arbeit', 'durchstich', 'fertig', 'prod'].includes(n.status.key);
|
||||||
|
}
|
||||||
|
/* Angefangen und noch offen: realisiert, aber nicht erledigt (D61). Aus zwei
|
||||||
|
vorhandenen Begriffen zusammengesetzt statt einer dritten Schwelle. */
|
||||||
|
export function isStarted(n){ return isRealized(n) && !isDone(n); }
|
||||||
|
/* Wahlmenge einer disjunktiven Gruppe (SPEC §9, D61): Ist etwas realisiert,
|
||||||
|
ist die Wahl getroffen — dann wird nur noch unter den realisierten gewählt.
|
||||||
|
Sonst stehen alle zur Wahl. Die Kostenregel (kleinste rekursive Kosten,
|
||||||
|
Gleichstand ⇒ erste) gilt innerhalb der Menge unverändert. */
|
||||||
|
export function chosenPool(kids){
|
||||||
|
const real = kids.filter(isRealized);
|
||||||
|
return real.length ? real : kids;
|
||||||
|
}
|
||||||
/* fehlende Größe wird als M interpretiert; Erledigtes kostet nichts mehr */
|
/* fehlende Größe wird als M interpretiert; Erledigtes kostet nichts mehr */
|
||||||
export function ownCost(n){ return isDone(n) ? 0 : SIZE_RANK[n.size || 'M'] + 1; }
|
export function ownCost(n){ return isDone(n) ? 0 : SIZE_RANK[n.size || 'M'] + 1; }
|
||||||
export function cheapestCost(n){
|
export function cheapestCost(n){
|
||||||
const kids = pathChildren(n);
|
const kids = pathChildren(n);
|
||||||
let c = ownCost(n);
|
let c = ownCost(n);
|
||||||
if(kids.length){
|
if(kids.length){
|
||||||
if(gateOf(kids) !== 'and') c += Math.min(...kids.map(cheapestCost));
|
if(gateOf(kids) !== 'and') c += Math.min(...chosenPool(kids).map(cheapestCost));
|
||||||
else c += kids.reduce((s, k) => s + cheapestCost(k), 0);
|
else c += kids.reduce((s, k) => s + cheapestCost(k), 0);
|
||||||
}
|
}
|
||||||
return c;
|
return c;
|
||||||
@@ -129,10 +149,14 @@ export function computeCheapPlan(roots){
|
|||||||
}
|
}
|
||||||
})(roots);
|
})(roots);
|
||||||
for(const kids of groups){
|
for(const kids of groups){
|
||||||
let best = kids[0], bc = cheapestCost(kids[0]);
|
const pool = chosenPool(kids);
|
||||||
for(const k of kids.slice(1)){ const c = cheapestCost(k); if(c < bc){ bc = c; best = k; } }
|
let best = pool[0], bc = cheapestCost(pool[0]);
|
||||||
|
for(const k of pool.slice(1)){ const c = cheapestCost(k); if(c < bc){ bc = c; best = k; } }
|
||||||
localChoice.set(kids[0], best); /* Schlüssel: erstes Kind der Gruppe */
|
localChoice.set(kids[0], best); /* Schlüssel: erstes Kind der Gruppe */
|
||||||
if(anyDeps && kids.some(k => touches.get(k))) coupled.push(kids);
|
/* Eine entschiedene Gruppe (genau eine realisierte Alternative, D61) ist
|
||||||
|
keine freie Variable mehr — sie koppelt nicht und verkleinert die Suche. */
|
||||||
|
if(anyDeps && pool.length > 1 && pool.some(k => touches.get(k)))
|
||||||
|
coupled.push({key: kids[0], pool});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Nötige Menge für eine Belegung der gekoppelten Gruppen. */
|
/* Nötige Menge für eine Belegung der gekoppelten Gruppen. */
|
||||||
@@ -159,7 +183,7 @@ export function computeCheapPlan(roots){
|
|||||||
const costOf = set => { let c = 0; set.forEach(n => c += ownCost(n)); return c; };
|
const costOf = set => { let c = 0; set.forEach(n => c += ownCost(n)); return c; };
|
||||||
|
|
||||||
let product = 1;
|
let product = 1;
|
||||||
for(const kids of coupled){ product *= kids.length; if(product > EXACT_LIMIT) break; }
|
for(const grp of coupled){ product *= grp.pool.length; if(product > EXACT_LIMIT) break; }
|
||||||
if(product > EXACT_LIMIT){
|
if(product > EXACT_LIMIT){
|
||||||
/* Gierig, aber benannt (D42): lokale Wahl überall, Hülle trotzdem. */
|
/* Gierig, aber benannt (D42): lokale Wahl überall, Hülle trotzdem. */
|
||||||
return {set: needed(new Map()), exact: false};
|
return {set: needed(new Map()), exact: false};
|
||||||
@@ -171,12 +195,12 @@ export function computeCheapPlan(roots){
|
|||||||
let best = null, bc = Infinity;
|
let best = null, bc = Infinity;
|
||||||
for(;;){
|
for(;;){
|
||||||
const choice = new Map();
|
const choice = new Map();
|
||||||
coupled.forEach((kids, g) => choice.set(kids[0], kids[idx[g]]));
|
coupled.forEach((grp, i) => choice.set(grp.key, grp.pool[idx[i]]));
|
||||||
const set = needed(choice);
|
const set = needed(choice);
|
||||||
const c = costOf(set);
|
const c = costOf(set);
|
||||||
if(c < bc){ bc = c; best = set; }
|
if(c < bc){ bc = c; best = set; }
|
||||||
let g = coupled.length - 1;
|
let g = coupled.length - 1;
|
||||||
while(g >= 0 && ++idx[g] >= coupled[g].length){ idx[g] = 0; g--; }
|
while(g >= 0 && ++idx[g] >= coupled[g].pool.length){ idx[g] = 0; g--; }
|
||||||
if(g < 0) break;
|
if(g < 0) break;
|
||||||
}
|
}
|
||||||
return {set: best, exact: true};
|
return {set: best, exact: true};
|
||||||
|
|||||||
@@ -80,10 +80,22 @@ describe('Auswahl — die getroffene Wahl gewinnt', () => {
|
|||||||
| [ ] Billig (XS)`)).toEqual(['Gemacht', 'Wahl']);
|
| [ ] Billig (XS)`)).toEqual(['Gemacht', 'Wahl']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('eine erst angefangene Alternative gewinnt dadurch NICHT', () => {
|
/* Bis D61 gewann hier „Billig": Die Wahl lief allein über die Kosten, und
|
||||||
|
die sind nur bei `[x]`/`[^]` null. Angefangenes kostet voll und verlor
|
||||||
|
damit — obwohl SPEC §9 die Regel schon so formuliert hatte. */
|
||||||
|
it('gilt auch für eine erst angefangene Alternative', () => {
|
||||||
expect(cheapLabels(`[ ] Wahl (XS)
|
expect(cheapLabels(`[ ] Wahl (XS)
|
||||||
| [~] Angefangen (L)
|
| [~] Angefangen (L)
|
||||||
| [ ] Billig (XS)`)).toEqual(['Billig', 'Wahl']);
|
| [ ] Billig (XS)`)).toEqual(['Angefangen', 'Wahl']);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Die unangetastete ist hier die billigste von allen — gewählt wird
|
||||||
|
trotzdem unter den realisierten, und dort entscheiden die Kosten. */
|
||||||
|
it('lässt unter mehreren realisierten wieder die Kosten entscheiden', () => {
|
||||||
|
expect(cheapLabels(`[ ] Wahl (XS)
|
||||||
|
| [~] Teuer (L)
|
||||||
|
| [/] Günstig (S)
|
||||||
|
| [ ] Unangetastet (XS)`)).toEqual(['Günstig', 'Wahl']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('gilt auch in einer XOR-Gruppe', () => {
|
it('gilt auch in einer XOR-Gruppe', () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { parse } from '../src/parser.js';
|
import { parse } from '../src/parser.js';
|
||||||
import { computeCheapSet, cheapestCost, visibleChildren } from '../src/model.js';
|
import { computeCheapSet, cheapestCost, cheapCls, visibleChildren } from '../src/model.js';
|
||||||
import { renderTreeHtml } from '../src/render.js';
|
import { renderTreeHtml } from '../src/render.js';
|
||||||
|
|
||||||
const t = key => key;
|
const t = key => key;
|
||||||
@@ -38,7 +38,7 @@ describe('Parser — `+` setzt optional, nicht das Gate', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Günstigster Pfad — optionale Knoten sind nie nötig', () => {
|
describe('Günstigster Pfad — unangetastete Zugaben sind nicht nötig', () => {
|
||||||
const BAUM = `[ ] Wurzel (S)
|
const BAUM = `[ ] Wurzel (S)
|
||||||
- [ ] Pflicht (S)
|
- [ ] Pflicht (S)
|
||||||
+ [ ] Zugabe (XXL)
|
+ [ ] Zugabe (XXL)
|
||||||
@@ -69,6 +69,59 @@ describe('Günstigster Pfad — optionale Knoten sind nie nötig', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Die Ausnahme (D61): Wird an der Zugabe gearbeitet, ist sie offene Arbeit —
|
||||||
|
und der Pfad zeigt seit D46 die offene Front. Erledigte bleiben draußen. */
|
||||||
|
describe('Günstigster Pfad — an einer angefangenen Zugabe wird gearbeitet', () => {
|
||||||
|
it('nimmt eine `[~]`-Zugabe auf den Pfad', () => {
|
||||||
|
expect(cheapLabels(`[ ] Wurzel (S)\n - [ ] Pflicht (S)\n + [~] Zugabe (S)`))
|
||||||
|
.toEqual(['Pflicht', 'Wurzel', 'Zugabe']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nimmt eine `[/]`-Zugabe ebenso', () => {
|
||||||
|
expect(cheapLabels(`[ ] Wurzel (S)\n + [/] Zugabe (S)`))
|
||||||
|
.toEqual(['Wurzel', 'Zugabe']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt `[?]`, `[ ]`, `[!]` und ohne Status weiterhin draußen', () => {
|
||||||
|
expect(cheapLabels(`[ ] Wurzel (S)
|
||||||
|
+ [?] Idee (S)
|
||||||
|
+ [ ] Geplant (S)
|
||||||
|
+ [!] Riskant (S)
|
||||||
|
+ Neutral (S)`)).toEqual(['Wurzel']);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Genau der Punkt, an dem die Empfehlung korrigiert wurde: Fertiges gehört
|
||||||
|
nicht auf die offene Front — und was darunter offen blieb, ist mit der
|
||||||
|
Zugabe zusammen entbehrlich (§3). */
|
||||||
|
it('lässt eine erledigte Zugabe samt offenem Rest draußen', () => {
|
||||||
|
expect(cheapLabels(`[ ] Wurzel (S)
|
||||||
|
+ [x] Fertige Zugabe (S)
|
||||||
|
- [ ] Rest (M)
|
||||||
|
+ [^] Live (S)`)).toEqual(['Wurzel']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nimmt den Teilbaum der angefangenen Zugabe mit', () => {
|
||||||
|
expect(cheapLabels(`[ ] Wurzel (S)\n + [~] Zugabe (S)\n - [ ] Teil (S)`))
|
||||||
|
.toEqual(['Teil', 'Wurzel', 'Zugabe']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rechnet sie damit auch in die Kosten des Elternknotens', () => {
|
||||||
|
/* Wurzel (S=2) + Zugabe (S=2) = 4; unangetastet wären es 2. */
|
||||||
|
expect(cheapestCost(roots(`[ ] Wurzel (S)\n + [~] Zugabe (S)`)[0])).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('macht sie zur Station statt des Elternknotens', () => {
|
||||||
|
const [wurzel] = roots(`[ ] Wurzel (S)\n + [~] Zugabe (S)`);
|
||||||
|
const set = computeCheapSet([wurzel]);
|
||||||
|
expect(cheapCls(wurzel, set, false)).toBe('cheap');
|
||||||
|
expect(cheapCls(wurzel.children[0], set, false)).toBe('cheap cheap-leaf');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt eine verworfene Zugabe auch angefangen draußen', () => {
|
||||||
|
expect(cheapLabels(`[ ] Wurzel (S)\n + [-] Abgebrochen (S)`)).toEqual(['Wurzel']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Renderer — Kennzeichnung und Gemischt-Warnung', () => {
|
describe('Renderer — Kennzeichnung und Gemischt-Warnung', () => {
|
||||||
it('gibt dem optionalen Knoten die Klasse `opt`', () => {
|
it('gibt dem optionalen Knoten die Klasse `opt`', () => {
|
||||||
const {html} = render(`[ ] Wurzel\n - [ ] Pflicht\n + [ ] Zugabe`);
|
const {html} = render(`[ ] Wurzel\n - [ ] Pflicht\n + [ ] Zugabe`);
|
||||||
|
|||||||
Reference in New Issue
Block a user