feat: Günstigster Pfad auf der Dependency Closure — exakt über gekoppelte Gruppen (SPEC §9, D42)

Jeder nötige Knoten zieht seine :#…-Ziele samt Realisierung in die
nötige Menge; gemeinsam Gebrauchtes zählt über die Vereinigung nur
einmal. Verfahren gewählt und benannt (D34): erschöpfende Suche, aber
nur über die GEKOPPELTEN Gruppen (Teilbaum enthält Abhängigkeiten oder
wird gebraucht) — ungekoppelte wählen lokal wie bisher, ohne
Abhängigkeiten bleibt es bei genau einer Auswertung zum alten Preis.
Lexikografischer Odometer, strikt < : bei Gleichstand die erste
Alternative. Oberhalb des Suchlimits (20 000) gierig mit zeilenloser
Warnung cheapApprox statt stillschweigender Pseudo-Optimalität.
Abhängigkeiten ziehen auch optionale und nicht gewählte Ziele (ohne
deren Vorfahren), verworfene nie. Neue API computeCheapPlan {set,exact};
markCheapest entfällt. 8 neue Tests (tests/closure.test.js).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-22 19:10:42 +02:00
co-authored by Claude Fable 5
parent afe536e33c
commit a918c75286
7 changed files with 300 additions and 22 deletions
+41
View File
@@ -1985,3 +1985,44 @@ Kante — ein Kreis von einem Knoten zu sich selbst sagte nichts.
auf dem DOM; headless getestet sind die `data-id`/`data-deps`-Attribute des
Renderers, geprüft wurde im Browser (alle drei Modi, Fokus- und
Cursor-Hervorhebung, Export).
## D42 — Closure-Pfad: erschöpfend über die gekoppelten Gruppen, gierig nur benannt
Die letzte Baustelle aus D34: Mit Abhängigkeiten zählt der günstigste Pfad
nicht mehr den gewählten Teilbaum, sondern die **Hülle** — jeder nötige Knoten
zieht seine Ziele samt Realisierung nach, gemeinsam Gebrauchtes zählt über die
Mengen-Vereinigung nur einmal. Damit ist die Wahl je Alternativgruppe nicht
mehr lokal optimal (das D34-Beispiel: `A (S) :#db` schlägt `B (M)`, sobald
`#db` ohnehin bezahlt wird). Die Entscheidungen:
**Verfahren: erschöpfende Suche — aber nur über die gekoppelten Gruppen.**
D34 stellte „exakt, die Bäume sind klein“ gegen „gierig, aber benannt“;
gebaut sind beide, mit einer Beobachtung dazwischen, die die exakte Suche
praktisch immer billig macht: **Nur Gruppen, in deren Teilbäumen
Abhängigkeiten stehen oder auf deren Knoten welche zeigen, koppeln
überhaupt.** Alle übrigen wählen weiterhin lokal (kleinste rekursive Kosten,
Gleichstand ⇒ erste) — ohne Abhängigkeiten gibt es null gekoppelte Gruppen
und genau eine Auswertung, also exakt das alte Verhalten zum alten Preis.
Über die gekoppelten Gruppen läuft ein lexikografischer Odometer (frühe
Gruppen wechseln zuletzt, strikt `<` gewinnt — bei Gleichstand bleibt so die
erste Alternative, §9). Übersteigt das Produkt der Gruppengrößen das
Suchlimit (20 000), fällt die Rechnung auf die gierige lokale Wahl zurück
und **sagt es**: zeilenlose Warnung `cheapApprox` — die in D34 verlangte
Benennung, statt stillschweigend Optimalität zu suggerieren.
**Abhängigkeiten ziehen, was sie brauchen — auch Optionales und nicht
Gewähltes.** `+` heißt „für das Ganze entbehrlich“, aber wer per `:#…` darauf
zeigt, braucht es eben doch; und ein Ziel in einer nicht gewählten
Alternative wird trotzdem realisiert, wenn etwas Nötiges davon abhängt. Im
Diagramm bleibt so ein einzelner heller Knoten im zurückgetretenen Zweig
stehen — sichtbar „das hier wird gebraucht, egal wie ihr wählt“. Nur
**verworfene** Ziele werden nie gezogen (§9: verworfen zählt nie): Sie werden
nicht realisiert, und dass der Abhängige deshalb nie fertig wird, sagt schon
der effektive Status (D39) — der Pfad muss die Lüge nicht einpreisen.
Gezogen wird das Ziel samt **Abwärts**-Realisierung, nicht seine Vorfahren:
Der Elternknoten braucht sein Kind, nicht umgekehrt.
**API: `computeCheapPlan(roots)` → `{set, exact}`.** `computeCheapSet` bleibt
als Hülle für Tests und Kompatibilität; `render()` liest `exact` für die
Warnung. `markCheapest` entfällt — die Menge entsteht jetzt in einem
Worklist-Durchlauf je Belegung (Zyklen enden über die Mengen-Prüfung von
selbst; erste ID-Vergabe gewinnt, D36/D39).