Files
werkbaum/frontend/tests/closure.test.js
T
mhoennigandClaude Fable 5 a918c75286 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>
2026-08-22 19:10:42 +02:00

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);
});
});