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
+19
-2
@@ -1,6 +1,6 @@
|
||||
import './style.css';
|
||||
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 { formatWarning } from './warnings.js';
|
||||
import { padUrls } from './remote.js';
|
||||
@@ -90,7 +90,15 @@ function render(){
|
||||
freshSet = new Set();
|
||||
foldByLine = new Map();
|
||||
} 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);
|
||||
/* Die Menge MUSS aus denselben Knotenobjekten gebildet werden, die gerade
|
||||
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.",
|
||||
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.",
|
||||
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",
|
||||
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.",
|
||||
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.",
|
||||
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",
|
||||
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.",
|
||||
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.",
|
||||
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",
|
||||
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.",
|
||||
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.",
|
||||
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é",
|
||||
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.",
|
||||
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.",
|
||||
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",
|
||||
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
|
||||
@@ -1652,6 +1665,7 @@ const I18N = {
|
||||
unknownDepWarn:"Строка {line}: зависимость #{id} — узла с таким ID нет.",
|
||||
unknownDescWarn:"Строка {line}: описание для #{id} — узла с таким ID нет.",
|
||||
descStrayWarn:"Строка {line}: строка описания без привязки — перед ней нет узла или блока #id.",
|
||||
cheapApproxWarn:"Слишком много связанных групп альтернатив для точного поиска — самый дешёвый путь оценён жадно (локальный выбор в каждой группе).",
|
||||
st_idee:"идея", st_geplant:"запланировано", st_arbeit:"в работе", st_durchstich:"сквозной прототип",
|
||||
st_fertig:"готово", st_prod:"в эксплуатации", st_highrisk:"высокий риск", st_verworfen:"отклонено",
|
||||
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
|
||||
@@ -1727,6 +1741,7 @@ const I18N = {
|
||||
unknownDepWarn:"पंक्ति {line}: निर्भरता #{id} — इस आईडी वाला कोई नोड नहीं है।",
|
||||
unknownDescWarn:"पंक्ति {line}: #{id} के लिए विवरण — इस आईडी वाला कोई नोड नहीं है।",
|
||||
descStrayWarn:"पंक्ति {line}: विवरण पंक्ति बिना संदर्भ — इससे पहले कोई नोड या #id ब्लॉक नहीं है।",
|
||||
cheapApproxWarn:"सटीक खोज के लिए बहुत सारे युग्मित विकल्प-समूह — सबसे सस्ता पथ लालची अनुमान है (प्रति समूह स्थानीय चयन)।",
|
||||
st_idee:"विचार", st_geplant:"नियोजित", st_arbeit:"प्रगति पर", st_durchstich:"कार्यशील ढाँचा",
|
||||
st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_highrisk:"उच्च जोखिम", st_verworfen:"अस्वीकृत",
|
||||
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
|
||||
@@ -1802,6 +1817,7 @@ const I18N = {
|
||||
unknownDepWarn:"第 {line} 行:依赖 #{id}——没有节点使用此 ID。",
|
||||
unknownDescWarn:"第 {line} 行:#{id} 的描述——没有节点使用此 ID。",
|
||||
descStrayWarn:"第 {line} 行:描述行没有归属——前面缺少节点或 #id 块。",
|
||||
cheapApproxWarn:"耦合的备选组过多,无法精确搜索——最便宜路径为贪心估计(每组就地选择)。",
|
||||
st_idee:"想法", st_geplant:"已计划", st_arbeit:"进行中", st_durchstich:"可运行骨架",
|
||||
st_fertig:"已完成", st_prod:"已上线", st_highrisk:"高风险", st_verworfen:"已放弃",
|
||||
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
|
||||
@@ -1877,6 +1893,7 @@ const I18N = {
|
||||
unknownDepWarn:"{line} 行目:依存 #{id} — この ID を持つノードはありません。",
|
||||
unknownDescWarn:"{line} 行目:#{id} の説明 — この ID を持つノードはありません。",
|
||||
descStrayWarn:"{line} 行目:説明行の帰属先がありません — 直前にノードまたは #id ブロックが必要です。",
|
||||
cheapApproxWarn:"結合された選択肢グループが多すぎるため厳密探索は不可 — 最安パスは貪欲法による推定です(グループごとに局所選択)。",
|
||||
st_idee:"アイデア", st_geplant:"計画済み", st_arbeit:"作業中", st_durchstich:"ウォーキングスケルトン",
|
||||
st_fertig:"完了", st_prod:"本番稼働", st_highrisk:"高リスク", st_verworfen:"破棄",
|
||||
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
|
||||
|
||||
+110
-14
@@ -52,23 +52,119 @@ export function cheapestCost(n){
|
||||
}
|
||||
return c;
|
||||
}
|
||||
export function markCheapest(n, set){
|
||||
set.add(n);
|
||||
const kids = pathChildren(n);
|
||||
if(!kids.length) return;
|
||||
if(gateOf(kids) !== 'and'){
|
||||
let best = null, bc = Infinity;
|
||||
for(const k of kids){ const c = cheapestCost(k); if(c < bc){ bc = c; best = k; } }
|
||||
if(best) markCheapest(best, set);
|
||||
} else {
|
||||
for(const k of kids) markCheapest(k, 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);
|
||||
const kids = pathChildren(n);
|
||||
if(kids.length){
|
||||
if(gateOf(kids) !== 'and'){
|
||||
queue.push(kids.length === 1 ? kids[0]
|
||||
: (choice.get(kids[0]) || localChoice.get(kids[0])));
|
||||
} else for(const k of kids) queue.push(k);
|
||||
}
|
||||
for(const d of n.deps || []){
|
||||
const t = byId.get(d);
|
||||
if(t && !(t.status && t.status.key === 'verworfen')) queue.push(t);
|
||||
}
|
||||
}
|
||||
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. */
|
||||
/* Menge der nötigen Knoten über alle Wurzeln (Rückgabe wie bisher). */
|
||||
export function computeCheapSet(roots){
|
||||
const set = new Set();
|
||||
roots.forEach(r => markCheapest(r, set));
|
||||
return set;
|
||||
return computeCheapPlan(roots).set;
|
||||
}
|
||||
/* CSS-Klassen für den günstigen Pfad. Leere `cheapSet` (Pfad aus) ⇒ ''.
|
||||
Endknoten (kein Kind liegt auf dem Pfad) bekommt zusätzlich 'cheap-leaf'. */
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
Beschreibungsteil ohne #id-Block; meldet
|
||||
auch die von einem versehentlichen
|
||||
`---` 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
|
||||
Zeilennummer, erscheint dadurch zuoberst
|
||||
- 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)});
|
||||
case 'descStray':
|
||||
return t('descStrayWarn', {line: w.line});
|
||||
case 'cheapApprox':
|
||||
return t('cheapApproxWarn');
|
||||
case 'sourceLoad':
|
||||
return t('sourceLoadWarn', {url: esc(w.url), error: esc(w.error)});
|
||||
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