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>
99 lines
3.6 KiB
JavaScript
99 lines
3.6 KiB
JavaScript
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);
|
|
});
|
|
});
|