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:
co-authored by
Claude Fable 5
parent
afe536e33c
commit
a918c75286
@@ -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
|
auf dem DOM; headless getestet sind die `data-id`/`data-deps`-Attribute des
|
||||||
Renderers, geprüft wurde im Browser (alle drei Modi, Fokus- und
|
Renderers, geprüft wurde im Browser (alle drei Modi, Fokus- und
|
||||||
Cursor-Hervorhebung, Export).
|
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).
|
||||||
|
|||||||
+21
-5
@@ -423,6 +423,24 @@ werden die für die günstigste Realisierung **nötigen** Knoten:
|
|||||||
- **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
|
||||||
SPEC-Semantik der Größen in §5 bleibt unberührt).
|
SPEC-Semantik der Größen in §5 bleibt unberührt).
|
||||||
|
|
||||||
|
**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
|
||||||
|
Gebrauchtes zählt über die Mengen-Vereinigung nur **einmal**. Regeln:
|
||||||
|
|
||||||
|
- Abhängigkeiten ziehen ihr Ziel auch dann, wenn es **optional** ist oder in
|
||||||
|
einer **nicht gewählten Alternative** steht — gebraucht ist gebraucht; im
|
||||||
|
Diagramm bleibt so ein einzelner heller Knoten in einem zurückgetretenen
|
||||||
|
Zweig stehen. Nur **verworfene** Ziele werden nie gezogen (verworfen zählt
|
||||||
|
nie); dass so ein Knoten nicht fertig werden kann, zeigt der effektive
|
||||||
|
Status (§4).
|
||||||
|
- Damit ist die Wahl je Alternativgruppe **nicht mehr lokal**: Eine teurere
|
||||||
|
Alternative kann gewinnen, weil ihre Abhängigkeiten anderswo ohnehin
|
||||||
|
bezahlt werden. **Verfahren (D42): erschöpfende Suche** über die
|
||||||
|
**gekoppelten** Gruppen (Gruppen, deren Teilbäume Abhängigkeiten enthalten
|
||||||
|
oder gebraucht werden); alle übrigen wählen lokal wie bisher, bei
|
||||||
|
Gleichstand die erste. Wird die Suche zu groß, rechnet die Anzeige **gierig
|
||||||
|
und sagt es** — Warnung `cheapApprox`, zeilenlos.
|
||||||
|
|
||||||
Darstellung per **Inversion**: nicht benötigte Knoten (nicht-gewählte
|
Darstellung per **Inversion**: nicht benötigte Knoten (nicht-gewählte
|
||||||
any-of-Alternativen und optionale Knoten, je samt Teilbaum) treten zurück
|
any-of-Alternativen und optionale Knoten, je samt Teilbaum) treten zurück
|
||||||
(blass, entsättigt); der
|
(blass, entsättigt); der
|
||||||
@@ -758,11 +776,9 @@ Knoten, Hervorhebung am ausgewählten Knoten. Begründung: D41.
|
|||||||
|
|
||||||
### Günstigster Pfad mit Abhängigkeiten
|
### Günstigster Pfad mit Abhängigkeiten
|
||||||
|
|
||||||
Die Kostenrechnung aus §9 zählt heute nur den gewählten Teilbaum. Mit
|
**Umgesetzt** — siehe §9 (Dependency Closure): Vereinigung statt Teilbaum,
|
||||||
Abhängigkeiten zählt die **Dependency Closure**: alles, was zusätzlich nötig
|
gemeinsam Gebrauchtes zählt einmal, erschöpfende Suche über die gekoppelten
|
||||||
ist, damit der gewählte Knoten effektiv fertig werden kann. Gemeinsam
|
Gruppen mit benanntem gierigem Rückfall. Begründung: D42.
|
||||||
benötigte Abhängigkeiten werden dabei **nur einmal** gezählt. Genau das macht
|
|
||||||
die Rechnung schwerer als heute — siehe D34.
|
|
||||||
|
|
||||||
### Knotenbeschreibungen (`"` und `---`)
|
### Knotenbeschreibungen (`"` und `---`)
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -90,10 +90,15 @@ entscheiden, **bevor** Code entsteht.
|
|||||||
Minimum); **Knotenfarbe = effektiver Status**, bei Diskrepanz die eigene
|
Minimum); **Knotenfarbe = effektiver Status**, bei Diskrepanz die eigene
|
||||||
Statusbox als Marke unten links in den eigenen Farben (auch im Export);
|
Statusbox als Marke unten links in den eigenen Farben (auch im Export);
|
||||||
Tooltip + `a11yEffective`; `tests/effective.test.js`.
|
Tooltip + `a11yEffective`; `tests/effective.test.js`.
|
||||||
- [ ] Günstigsten Pfad auf die Dependency Closure umstellen (gemeinsame
|
- [x] Günstigsten Pfad auf die Dependency Closure umstellen (gemeinsame
|
||||||
Abhängigkeiten nur einmal zählen). Erweitert D18; die gierige Wahl je
|
Abhängigkeiten nur einmal zählen). Erweitert D18; die gierige Wahl je
|
||||||
Alternativgruppe ist damit nicht mehr optimal — Verfahren wählen und
|
Alternativgruppe ist damit nicht mehr optimal — Verfahren wählen und
|
||||||
**benennen**.
|
**benennen**.
|
||||||
|
→ Umgesetzt (D42): erschöpfende Suche über die gekoppelten Gruppen
|
||||||
|
(`computeCheapPlan` in model.js), ungekoppelte wählen lokal wie bisher;
|
||||||
|
oberhalb des Suchlimits gierig mit zeilenloser Warnung `cheapApprox`.
|
||||||
|
Abhängigkeiten ziehen auch optionale/nicht gewählte Ziele, verworfene
|
||||||
|
nie; `tests/closure.test.js`.
|
||||||
- [x] Querverbindungen zeichnen (eigene SVG-Ebene, optisch sekundär);
|
- [x] Querverbindungen zeichnen (eigene SVG-Ebene, optisch sekundär);
|
||||||
bei ausgewähltem Knoten ein-/ausgehende hervorheben.
|
bei ausgewähltem Knoten ein-/ausgehende hervorheben.
|
||||||
→ Umgesetzt (D41): gekrümmte blassgraue Kanten mit Pfeilspitze auf das
|
→ Umgesetzt (D41): gekrümmte blassgraue Kanten mit Pfeilspitze auf das
|
||||||
|
|||||||
+19
-2
@@ -1,6 +1,6 @@
|
|||||||
import './style.css';
|
import './style.css';
|
||||||
import { parse } from './parser.js';
|
import { parse } from './parser.js';
|
||||||
import { computeCheapSet, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus } from './model.js';
|
import { computeCheapPlan, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus } from './model.js';
|
||||||
import { esc, renderTreeHtml } from './render.js';
|
import { esc, renderTreeHtml } from './render.js';
|
||||||
import { formatWarning } from './warnings.js';
|
import { formatWarning } from './warnings.js';
|
||||||
import { padUrls } from './remote.js';
|
import { padUrls } from './remote.js';
|
||||||
@@ -90,7 +90,15 @@ function render(){
|
|||||||
freshSet = new Set();
|
freshSet = new Set();
|
||||||
foldByLine = new Map();
|
foldByLine = new Map();
|
||||||
} else {
|
} else {
|
||||||
const cheapSet = cheapPathOn ? computeCheapSet(roots) : new Set();
|
/* Günstigster Pfad auf der Dependency Closure (D42): scheitert die exakte
|
||||||
|
Suche an zu vielen gekoppelten Gruppen, wird die gierige Schätzung
|
||||||
|
BENANNT statt stillschweigend geliefert (zeilenlose Warnung). */
|
||||||
|
let cheapSet = new Set();
|
||||||
|
if(cheapPathOn){
|
||||||
|
const plan = computeCheapPlan(roots);
|
||||||
|
cheapSet = plan.set;
|
||||||
|
if(!plan.exact) warnings = warnings.concat([{type: 'cheapApprox'}]);
|
||||||
|
}
|
||||||
out.classList.toggle('cheap-on', cheapPathOn);
|
out.classList.toggle('cheap-on', cheapPathOn);
|
||||||
/* Die Menge MUSS aus denselben Knotenobjekten gebildet werden, die gerade
|
/* Die Menge MUSS aus denselben Knotenobjekten gebildet werden, die gerade
|
||||||
gerendert werden — `freshProdSet` liefert Knoten aus `roots`. Eine früher
|
gerendert werden — `freshProdSet` liefert Knoten aus `roots`. Eine früher
|
||||||
@@ -1277,6 +1285,7 @@ const I18N = {
|
|||||||
unknownDepWarn:"Zeile {line}: Abhängigkeit #{id} — es gibt keinen Knoten mit dieser ID.",
|
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.",
|
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.",
|
descStrayWarn:"Zeile {line}: Beschreibungszeile ohne Bezug — ihr fehlt der Knoten bzw. der #id-Block davor.",
|
||||||
|
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_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",
|
st_fertig:"fertig", st_prod:"in Produktion", st_highrisk:"High Risk", st_verworfen:"verworfen",
|
||||||
unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.",
|
unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.",
|
||||||
@@ -1352,6 +1361,7 @@ const I18N = {
|
|||||||
unknownDepWarn:"Line {line}: dependency #{id} — no node has this ID.",
|
unknownDepWarn:"Line {line}: dependency #{id} — no node has this ID.",
|
||||||
unknownDescWarn:"Line {line}: description for #{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.",
|
descStrayWarn:"Line {line}: description line with nothing to attach to — it needs a node or an #id block before it.",
|
||||||
|
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_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",
|
st_fertig:"done", st_prod:"in production", st_highrisk:"high risk", st_verworfen:"discarded",
|
||||||
unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.",
|
unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.",
|
||||||
@@ -1427,6 +1437,7 @@ const I18N = {
|
|||||||
unknownDepWarn:"Línea {line}: dependencia #{id} — ningún nodo tiene esta ID.",
|
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.",
|
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.",
|
descStrayWarn:"Línea {line}: línea de descripción sin referencia — le falta un nodo o un bloque #id delante.",
|
||||||
|
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_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",
|
st_fertig:"terminado", st_prod:"en producción", st_highrisk:"alto riesgo", st_verworfen:"descartado",
|
||||||
unknownStatusWarn:"Línea {line}: código de estado desconocido «{code}» — mostrado como neutral.",
|
unknownStatusWarn:"Línea {line}: código de estado desconocido «{code}» — mostrado como neutral.",
|
||||||
@@ -1502,6 +1513,7 @@ const I18N = {
|
|||||||
unknownDepWarn:"Ligne {line} : dépendance #{id} — aucun nœud ne porte cet ID.",
|
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.",
|
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.",
|
descStrayWarn:"Ligne {line} : ligne de description sans rattachement — il lui manque un nœud ou un bloc #id avant.",
|
||||||
|
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_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é",
|
st_fertig:"terminé", st_prod:"en production", st_highrisk:"risque élevé", st_verworfen:"abandonné",
|
||||||
unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.",
|
unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.",
|
||||||
@@ -1577,6 +1589,7 @@ const I18N = {
|
|||||||
unknownDepWarn:"Wiersz {line}: zależność #{id} — żaden węzeł nie ma tego ID.",
|
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.",
|
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.",
|
descStrayWarn:"Wiersz {line}: wiersz opisu bez odniesienia — brakuje węzła lub bloku #id przed nim.",
|
||||||
|
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_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",
|
st_fertig:"gotowe", st_prod:"w produkcji", st_highrisk:"wysokie ryzyko", st_verworfen:"odrzucone",
|
||||||
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
|
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
|
||||||
@@ -1652,6 +1665,7 @@ const I18N = {
|
|||||||
unknownDepWarn:"Строка {line}: зависимость #{id} — узла с таким ID нет.",
|
unknownDepWarn:"Строка {line}: зависимость #{id} — узла с таким ID нет.",
|
||||||
unknownDescWarn:"Строка {line}: описание для #{id} — узла с таким ID нет.",
|
unknownDescWarn:"Строка {line}: описание для #{id} — узла с таким ID нет.",
|
||||||
descStrayWarn:"Строка {line}: строка описания без привязки — перед ней нет узла или блока #id.",
|
descStrayWarn:"Строка {line}: строка описания без привязки — перед ней нет узла или блока #id.",
|
||||||
|
cheapApproxWarn:"Слишком много связанных групп альтернатив для точного поиска — самый дешёвый путь оценён жадно (локальный выбор в каждой группе).",
|
||||||
st_idee:"идея", st_geplant:"запланировано", st_arbeit:"в работе", st_durchstich:"сквозной прототип",
|
st_idee:"идея", st_geplant:"запланировано", st_arbeit:"в работе", st_durchstich:"сквозной прототип",
|
||||||
st_fertig:"готово", st_prod:"в эксплуатации", st_highrisk:"высокий риск", st_verworfen:"отклонено",
|
st_fertig:"готово", st_prod:"в эксплуатации", st_highrisk:"высокий риск", st_verworfen:"отклонено",
|
||||||
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
|
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
|
||||||
@@ -1727,6 +1741,7 @@ const I18N = {
|
|||||||
unknownDepWarn:"पंक्ति {line}: निर्भरता #{id} — इस आईडी वाला कोई नोड नहीं है।",
|
unknownDepWarn:"पंक्ति {line}: निर्भरता #{id} — इस आईडी वाला कोई नोड नहीं है।",
|
||||||
unknownDescWarn:"पंक्ति {line}: #{id} के लिए विवरण — इस आईडी वाला कोई नोड नहीं है।",
|
unknownDescWarn:"पंक्ति {line}: #{id} के लिए विवरण — इस आईडी वाला कोई नोड नहीं है।",
|
||||||
descStrayWarn:"पंक्ति {line}: विवरण पंक्ति बिना संदर्भ — इससे पहले कोई नोड या #id ब्लॉक नहीं है।",
|
descStrayWarn:"पंक्ति {line}: विवरण पंक्ति बिना संदर्भ — इससे पहले कोई नोड या #id ब्लॉक नहीं है।",
|
||||||
|
cheapApproxWarn:"सटीक खोज के लिए बहुत सारे युग्मित विकल्प-समूह — सबसे सस्ता पथ लालची अनुमान है (प्रति समूह स्थानीय चयन)।",
|
||||||
st_idee:"विचार", st_geplant:"नियोजित", st_arbeit:"प्रगति पर", st_durchstich:"कार्यशील ढाँचा",
|
st_idee:"विचार", st_geplant:"नियोजित", st_arbeit:"प्रगति पर", st_durchstich:"कार्यशील ढाँचा",
|
||||||
st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_highrisk:"उच्च जोखिम", st_verworfen:"अस्वीकृत",
|
st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_highrisk:"उच्च जोखिम", st_verworfen:"अस्वीकृत",
|
||||||
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
|
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
|
||||||
@@ -1802,6 +1817,7 @@ const I18N = {
|
|||||||
unknownDepWarn:"第 {line} 行:依赖 #{id}——没有节点使用此 ID。",
|
unknownDepWarn:"第 {line} 行:依赖 #{id}——没有节点使用此 ID。",
|
||||||
unknownDescWarn:"第 {line} 行:#{id} 的描述——没有节点使用此 ID。",
|
unknownDescWarn:"第 {line} 行:#{id} 的描述——没有节点使用此 ID。",
|
||||||
descStrayWarn:"第 {line} 行:描述行没有归属——前面缺少节点或 #id 块。",
|
descStrayWarn:"第 {line} 行:描述行没有归属——前面缺少节点或 #id 块。",
|
||||||
|
cheapApproxWarn:"耦合的备选组过多,无法精确搜索——最便宜路径为贪心估计(每组就地选择)。",
|
||||||
st_idee:"想法", st_geplant:"已计划", st_arbeit:"进行中", st_durchstich:"可运行骨架",
|
st_idee:"想法", st_geplant:"已计划", st_arbeit:"进行中", st_durchstich:"可运行骨架",
|
||||||
st_fertig:"已完成", st_prod:"已上线", st_highrisk:"高风险", st_verworfen:"已放弃",
|
st_fertig:"已完成", st_prod:"已上线", st_highrisk:"高风险", st_verworfen:"已放弃",
|
||||||
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
|
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
|
||||||
@@ -1877,6 +1893,7 @@ const I18N = {
|
|||||||
unknownDepWarn:"{line} 行目:依存 #{id} — この ID を持つノードはありません。",
|
unknownDepWarn:"{line} 行目:依存 #{id} — この ID を持つノードはありません。",
|
||||||
unknownDescWarn:"{line} 行目:#{id} の説明 — この ID を持つノードはありません。",
|
unknownDescWarn:"{line} 行目:#{id} の説明 — この ID を持つノードはありません。",
|
||||||
descStrayWarn:"{line} 行目:説明行の帰属先がありません — 直前にノードまたは #id ブロックが必要です。",
|
descStrayWarn:"{line} 行目:説明行の帰属先がありません — 直前にノードまたは #id ブロックが必要です。",
|
||||||
|
cheapApproxWarn:"結合された選択肢グループが多すぎるため厳密探索は不可 — 最安パスは貪欲法による推定です(グループごとに局所選択)。",
|
||||||
st_idee:"アイデア", st_geplant:"計画済み", st_arbeit:"作業中", st_durchstich:"ウォーキングスケルトン",
|
st_idee:"アイデア", st_geplant:"計画済み", st_arbeit:"作業中", st_durchstich:"ウォーキングスケルトン",
|
||||||
st_fertig:"完了", st_prod:"本番稼働", st_highrisk:"高リスク", st_verworfen:"破棄",
|
st_fertig:"完了", st_prod:"本番稼働", st_highrisk:"高リスク", st_verworfen:"破棄",
|
||||||
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
|
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
|
||||||
|
|||||||
+108
-12
@@ -52,23 +52,119 @@ export function cheapestCost(n){
|
|||||||
}
|
}
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
export function markCheapest(n, set){
|
/* ---------- Günstigster Pfad auf der Dependency Closure (SPEC §9, D42) ----
|
||||||
|
Mit Abhängigkeiten zählt nicht mehr der gewählte Teilbaum, sondern die
|
||||||
|
HÜLLE: Jeder nötige Knoten zieht seine `:#…`-Ziele samt deren Realisierung
|
||||||
|
nach; gemeinsam Gebrauchtes zählt über die Mengen-Vereinigung nur EINMAL.
|
||||||
|
Damit ist die Wahl je Alternativgruppe nicht mehr lokal optimal (D34).
|
||||||
|
|
||||||
|
Verfahren — gewählt und benannt (D42): ERSCHÖPFENDE SUCHE, aber nur über
|
||||||
|
die GEKOPPELTEN Gruppen — disjunktive Gruppen, in deren Teilbäumen
|
||||||
|
Abhängigkeiten stehen oder auf deren Knoten Abhängigkeiten zeigen. Alle
|
||||||
|
übrigen Gruppen sind von der Kopplung unberührt und wählen lokal wie
|
||||||
|
bisher (kleinste rekursive Kosten, Gleichstand ⇒ erste). Ohne
|
||||||
|
Abhängigkeiten gibt es keine gekoppelten Gruppen und genau eine
|
||||||
|
Auswertung — das alte Verhalten. Übersteigt das Produkt der
|
||||||
|
Gruppengrößen EXACT_LIMIT, fällt die Rechnung auf die gierige lokale
|
||||||
|
Wahl zurück und SAGT es (`exact:false` ⇒ Warnung `cheapApprox`).
|
||||||
|
|
||||||
|
Regeln der Hülle: Abhängigkeiten ziehen ihr Ziel auch dann, wenn es
|
||||||
|
optional ist oder in einer nicht gewählten Alternative steht — gebraucht
|
||||||
|
ist gebraucht; nur verworfene Ziele nie (§9: verworfen zählt nie), deren
|
||||||
|
Unerfüllbarkeit zeigt der effektive Status (D39). Bei doppelter ID gilt
|
||||||
|
die erste Vergabe (D36); Zyklen enden über die Mengen-Prüfung von selbst. */
|
||||||
|
const EXACT_LIMIT = 20000;
|
||||||
|
export function computeCheapPlan(roots){
|
||||||
|
const nodes = [], byId = new Map(), referenced = new Set();
|
||||||
|
(function walk(ns){
|
||||||
|
for(const n of ns){
|
||||||
|
nodes.push(n);
|
||||||
|
if(n.id != null && !byId.has(n.id)) byId.set(n.id, n);
|
||||||
|
walk(n.children);
|
||||||
|
}
|
||||||
|
})(roots);
|
||||||
|
for(const n of nodes) for(const d of n.deps || []) referenced.add(d);
|
||||||
|
const anyDeps = referenced.size > 0;
|
||||||
|
|
||||||
|
/* Teilbaum berührt die Kopplung? (hat Abhängigkeiten oder wird gebraucht) */
|
||||||
|
const touches = new Map();
|
||||||
|
const touch = n => {
|
||||||
|
let v = (n.deps && n.deps.length > 0) || (n.id != null && referenced.has(n.id));
|
||||||
|
for(const k of n.children) v = touch(k) || v;
|
||||||
|
touches.set(n, v);
|
||||||
|
return v;
|
||||||
|
};
|
||||||
|
roots.forEach(touch);
|
||||||
|
|
||||||
|
/* Wahlpunkte: disjunktive Gruppen mit mehr als einer Alternative.
|
||||||
|
Lokale Wahl vorab (kleinste rekursive Kosten, Gleichstand ⇒ erste) —
|
||||||
|
sie gilt für ungekoppelte Gruppen und für den gierigen Rückfall. */
|
||||||
|
const coupled = [], localChoice = new Map();
|
||||||
|
const groups = [];
|
||||||
|
/* Nur Kindergruppen — Wurzeln sind immer alle nötig (wie bisher). */
|
||||||
|
(function groupsOf(ns){
|
||||||
|
for(const n of ns){
|
||||||
|
const kids = pathChildren(n);
|
||||||
|
if(kids.length > 1 && gateOf(kids) !== 'and') groups.push(kids);
|
||||||
|
groupsOf(n.children);
|
||||||
|
}
|
||||||
|
})(roots);
|
||||||
|
for(const kids of groups){
|
||||||
|
let best = kids[0], bc = cheapestCost(kids[0]);
|
||||||
|
for(const k of kids.slice(1)){ const c = cheapestCost(k); if(c < bc){ bc = c; best = k; } }
|
||||||
|
localChoice.set(kids[0], best); /* Schlüssel: erstes Kind der Gruppe */
|
||||||
|
if(anyDeps && kids.some(k => touches.get(k))) coupled.push(kids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nötige Menge für eine Belegung der gekoppelten Gruppen. */
|
||||||
|
const needed = choice => {
|
||||||
|
const set = new Set(), queue = [...roots];
|
||||||
|
while(queue.length){
|
||||||
|
const n = queue.pop();
|
||||||
|
if(set.has(n)) continue;
|
||||||
set.add(n);
|
set.add(n);
|
||||||
const kids = pathChildren(n);
|
const kids = pathChildren(n);
|
||||||
if(!kids.length) return;
|
if(kids.length){
|
||||||
if(gateOf(kids) !== 'and'){
|
if(gateOf(kids) !== 'and'){
|
||||||
let best = null, bc = Infinity;
|
queue.push(kids.length === 1 ? kids[0]
|
||||||
for(const k of kids){ const c = cheapestCost(k); if(c < bc){ bc = c; best = k; } }
|
: (choice.get(kids[0]) || localChoice.get(kids[0])));
|
||||||
if(best) markCheapest(best, set);
|
} else for(const k of kids) queue.push(k);
|
||||||
} else {
|
}
|
||||||
for(const k of kids) markCheapest(k, set);
|
for(const d of n.deps || []){
|
||||||
|
const t = byId.get(d);
|
||||||
|
if(t && !(t.status && t.status.key === 'verworfen')) queue.push(t);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
/* Menge der nötigen Knoten über alle Wurzeln. */
|
|
||||||
export function computeCheapSet(roots){
|
|
||||||
const set = new Set();
|
|
||||||
roots.forEach(r => markCheapest(r, set));
|
|
||||||
return set;
|
return set;
|
||||||
|
};
|
||||||
|
const costOf = set => { let c = 0; set.forEach(n => c += ownCost(n)); return c; };
|
||||||
|
|
||||||
|
let product = 1;
|
||||||
|
for(const kids of coupled){ product *= kids.length; if(product > EXACT_LIMIT) break; }
|
||||||
|
if(product > EXACT_LIMIT){
|
||||||
|
/* Gierig, aber benannt (D42): lokale Wahl überall, Hülle trotzdem. */
|
||||||
|
return {set: needed(new Map()), exact: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Erschöpfend, lexikografisch — frühere Gruppen wechseln zuletzt, strikt
|
||||||
|
kleiner gewinnt: Bei Gleichstand bleibt so die erste Alternative (§9). */
|
||||||
|
const idx = coupled.map(() => 0);
|
||||||
|
let best = null, bc = Infinity;
|
||||||
|
for(;;){
|
||||||
|
const choice = new Map();
|
||||||
|
coupled.forEach((kids, g) => choice.set(kids[0], kids[idx[g]]));
|
||||||
|
const set = needed(choice);
|
||||||
|
const c = costOf(set);
|
||||||
|
if(c < bc){ bc = c; best = set; }
|
||||||
|
let g = coupled.length - 1;
|
||||||
|
while(g >= 0 && ++idx[g] >= coupled[g].length){ idx[g] = 0; g--; }
|
||||||
|
if(g < 0) break;
|
||||||
|
}
|
||||||
|
return {set: best, exact: true};
|
||||||
|
}
|
||||||
|
/* Menge der nötigen Knoten über alle Wurzeln (Rückgabe wie bisher). */
|
||||||
|
export function computeCheapSet(roots){
|
||||||
|
return computeCheapPlan(roots).set;
|
||||||
}
|
}
|
||||||
/* CSS-Klassen für den günstigen Pfad. Leere `cheapSet` (Pfad aus) ⇒ ''.
|
/* CSS-Klassen für den günstigen Pfad. Leere `cheapSet` (Pfad aus) ⇒ ''.
|
||||||
Endknoten (kein Kind liegt auf dem Pfad) bekommt zusätzlich 'cheap-leaf'. */
|
Endknoten (kein Kind liegt auf dem Pfad) bekommt zusätzlich 'cheap-leaf'. */
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
Beschreibungsteil ohne #id-Block; meldet
|
Beschreibungsteil ohne #id-Block; meldet
|
||||||
auch die von einem versehentlichen
|
auch die von einem versehentlichen
|
||||||
`---` verschluckten Knotenzeilen
|
`---` verschluckten Knotenzeilen
|
||||||
|
- cheapApprox { } — günstigster Pfad nur gierig geschätzt:
|
||||||
|
zu viele gekoppelte Gruppen für die
|
||||||
|
exakte Suche (D42); zeilenlos
|
||||||
- sourceLoad { url, error } — ?sourceUrl= nicht ladbar (D23); ohne
|
- sourceLoad { url, error } — ?sourceUrl= nicht ladbar (D23); ohne
|
||||||
Zeilennummer, erscheint dadurch zuoberst
|
Zeilennummer, erscheint dadurch zuoberst
|
||||||
- padRateLimit { seconds } — zu früh nachgeladen; Werkbaum hat gar nicht
|
- padRateLimit { seconds } — zu früh nachgeladen; Werkbaum hat gar nicht
|
||||||
@@ -55,6 +58,8 @@ export function formatWarning(w, t){
|
|||||||
return t('unknownDescWarn', {line: w.line, id: esc(w.id)});
|
return t('unknownDescWarn', {line: w.line, id: esc(w.id)});
|
||||||
case 'descStray':
|
case 'descStray':
|
||||||
return t('descStrayWarn', {line: w.line});
|
return t('descStrayWarn', {line: w.line});
|
||||||
|
case 'cheapApprox':
|
||||||
|
return t('cheapApproxWarn');
|
||||||
case 'sourceLoad':
|
case 'sourceLoad':
|
||||||
return t('sourceLoadWarn', {url: esc(w.url), error: esc(w.error)});
|
return t('sourceLoadWarn', {url: esc(w.url), error: esc(w.error)});
|
||||||
case 'padRateLimit':
|
case 'padRateLimit':
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { parse } from '../src/parser.js';
|
||||||
|
import { computeCheapPlan, computeCheapSet } from '../src/model.js';
|
||||||
|
|
||||||
|
const roots = txt => parse(txt).roots;
|
||||||
|
const cheapLabels = txt => [...computeCheapSet(roots(txt))].map(n => n.label).sort();
|
||||||
|
|
||||||
|
/* Günstigster Pfad auf der Dependency Closure (SPEC §9, D42). */
|
||||||
|
describe('Closure — Abhängigkeiten ziehen ihr Ziel in die nötige Menge', () => {
|
||||||
|
it('zieht das Ziel samt seiner Realisierung nach', () => {
|
||||||
|
expect(cheapLabels(`[ ] W (XS)
|
||||||
|
- [ ] A (S) :#db
|
||||||
|
[ ] DB #db (S)
|
||||||
|
- [ ] Schema (XS)`))
|
||||||
|
.toEqual(['A', 'DB', 'Schema', 'W']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('zieht auch optionale und nicht gewählte Ziele — gebraucht ist gebraucht', () => {
|
||||||
|
const labels = cheapLabels(`[ ] W (XS)
|
||||||
|
- [ ] A (S) :#opt,#alt
|
||||||
|
+ [ ] Extra #opt (S)
|
||||||
|
- [ ] Wahl (XS)
|
||||||
|
| [ ] Billig (XS)
|
||||||
|
| [ ] Teuer (XL)
|
||||||
|
- [ ] Teil #alt (XS)`);
|
||||||
|
expect(labels).toContain('Extra');
|
||||||
|
expect(labels).toContain('Teil'); /* das Ziel selbst … */
|
||||||
|
expect(labels).not.toContain('Teuer'); /* … aber nicht sein Vorfahr */
|
||||||
|
expect(labels).toContain('Billig');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('zieht verworfene Ziele nie', () => {
|
||||||
|
expect(cheapLabels(`[ ] W (XS)\n - [ ] A (S) :#weg\n - [-] Weg #weg (XL)`))
|
||||||
|
.toEqual(['A', 'W']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Closure — gemeinsam Gebrauchtes zählt nur einmal', () => {
|
||||||
|
/* Das D34-Beispiel: A (S) :#db gegen B (M). Für sich verliert A, sobald
|
||||||
|
#db mehr als den Unterschied kostet — wird #db aber ohnehin gebraucht,
|
||||||
|
ist es bezahlt, und A gewinnt. */
|
||||||
|
/* DB liegt unter einer Zugabe — von sich aus ist sie also NICHT nötig,
|
||||||
|
sondern nur, wenn jemand per :#db darauf zeigt. */
|
||||||
|
const PLAN = braucht => `[ ] W (XS)
|
||||||
|
- [ ] Basis (XS)${braucht ? ' :#db' : ''}
|
||||||
|
- [ ] Wahl (XS)
|
||||||
|
| [ ] A (S) :#db
|
||||||
|
| [ ] B (M)
|
||||||
|
+ [ ] Fundus (XS)
|
||||||
|
- [ ] DB #db (L)`;
|
||||||
|
|
||||||
|
it('wählt lokal, wenn niemand sonst die Abhängigkeit bezahlt', () => {
|
||||||
|
expect(cheapLabels(PLAN(false))).not.toContain('A');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wählt die Alternative mit der schon bezahlten Abhängigkeit', () => {
|
||||||
|
const labels = cheapLabels(PLAN(true));
|
||||||
|
expect(labels).toContain('A');
|
||||||
|
expect(labels).not.toContain('B');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('koppelt auch zwei Gruppen über eine geteilte Abhängigkeit', () => {
|
||||||
|
/* Beide A-Alternativen teilen sich DB: A1+A2+DB = 2+2+2 = 6, B1+B2 =
|
||||||
|
3+3 = 6 — Gleichstand, und bei Gleichstand gewinnt die lexikografisch
|
||||||
|
erste Belegung (§9): beide Gruppen wählen A. Lokal (je Gruppe für sich:
|
||||||
|
S+S(db)=4 > M=3) hätte zweimal B gewonnen. */
|
||||||
|
const labels = cheapLabels(`[ ] W (XS)
|
||||||
|
- [ ] G1 (XS)
|
||||||
|
| [ ] A1 (S) :#db
|
||||||
|
| [ ] B1 (M)
|
||||||
|
- [ ] G2 (XS)
|
||||||
|
| [ ] A2 (S) :#db
|
||||||
|
| [ ] B2 (M)
|
||||||
|
[ ] DB #db (S)`);
|
||||||
|
expect(labels).toEqual(expect.arrayContaining(['A1', 'A2', 'DB']));
|
||||||
|
expect(labels).not.toContain('B1');
|
||||||
|
expect(labels).not.toContain('B2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Verfahren — exakt bis zum Limit, dann gierig und benannt', () => {
|
||||||
|
it('bleibt ohne Abhängigkeiten exakt mit genau dem alten Ergebnis', () => {
|
||||||
|
const plan = computeCheapPlan(roots(`[ ] W (XS)\n | [ ] A (L)\n | [ ] B (S)`));
|
||||||
|
expect(plan.exact).toBe(true);
|
||||||
|
expect([...plan.set].map(n => n.label).sort()).toEqual(['B', 'W']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('meldet die gierige Schätzung, wenn zu viele Gruppen koppeln', () => {
|
||||||
|
/* 15 gekoppelte Zweiergruppen: 2^15 = 32768 > 20000. */
|
||||||
|
let txt = `[ ] Ziel #z (S)\n`;
|
||||||
|
for(let g = 0; g < 15; g++){
|
||||||
|
txt += `[ ] G${g} (XS)\n | [ ] A${g} (S) :#z\n | [ ] B${g} (S)\n`;
|
||||||
|
}
|
||||||
|
const plan = computeCheapPlan(roots(txt));
|
||||||
|
expect(plan.exact).toBe(false);
|
||||||
|
expect(plan.set.size).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user