feat: Die Größe bepreist den ganzen Teilbaum — Kinder zählen nicht obendrauf (D69)
Der Preis eines Knotens ist seine angegebene (oder D66-geschätzte) Größe; die frühere Rekursion (eigene Größe plus Summe/Minimum der Kinder) zählte doppelt und bestrafte sorgfältig zerlegte Pakete — eine zerlegte S-Alternative verlor gegen ein grobes L. Das Vereinigungs-Maß der Closure-Suche (D42) zählt je Knoten nur noch die Marge über seine nötigen Teilpakete hinaus. Mitgelieferter Plan wählt identisch (per Vergleich gemessen); SPEC §9 und llms.md nachgezogen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
7278e74393
commit
4aaa19becd
@@ -111,6 +111,10 @@ One node per line. Everything except the label is optional.
|
||||
label. `"(L)"` and `((L))` mention a size literally (quoting convention).
|
||||
- From `(M)` upward a node **should be decomposed further**; a node ≥ M
|
||||
without children gets a placeholder hint in the diagram.
|
||||
- **A size prices its whole subtree.** For the cheapest path, the cost of a
|
||||
node is its own stated size — sub-packages are **not** added on top; the
|
||||
conflict check below guards whether they fit. A done node (`[x]`/`[^]`)
|
||||
costs 0.
|
||||
- For cost estimation a missing size is **estimated from the sub-packages**:
|
||||
at least the largest size among the counting children (same children as
|
||||
the conflict check below, except unsized children count too — estimated
|
||||
|
||||
+22
-10
@@ -118,15 +118,14 @@ export function assumedSize(n){
|
||||
}
|
||||
/* fehlende Größe wird geschätzt (assumedSize); Erledigtes kostet nichts mehr */
|
||||
export function ownCost(n){ return isDone(n) ? 0 : SIZE_RANK[assumedSize(n)] + 1; }
|
||||
export function cheapestCost(n){
|
||||
const kids = pathChildren(n);
|
||||
let c = ownCost(n);
|
||||
if(kids.length){
|
||||
if(gateOf(kids) !== 'and') c += Math.min(...chosenPool(kids).map(cheapestCost));
|
||||
else c += kids.reduce((s, k) => s + cheapestCost(k), 0);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
/* Die Größe bepreist den GANZEN Teilbaum (SPEC §9, D69): Wer ein Paket mit
|
||||
(S) bewertet hat, hat den Teilbaum bewertet — die Teilpakete kommen nicht
|
||||
noch einmal obendrauf. Ob sie in die Größe passen, prüft der
|
||||
Größen-Konflikt (§5/D62); fehlt die Größe, vertritt die Schätzung sie
|
||||
(assumedSize, D66). Der Preis einer Alternative ist damit schlicht ihr
|
||||
eigener — die frühere Rekursion (eigene Größe plus Summe/Minimum der
|
||||
Kinder) bestrafte gerade die sorgfältig zerlegten Pakete. */
|
||||
export function cheapestCost(n){ return ownCost(n); }
|
||||
/* ---------- 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
|
||||
@@ -216,7 +215,20 @@ export function computeCheapPlan(roots){
|
||||
}
|
||||
return set;
|
||||
};
|
||||
const costOf = set => { let c = 0; set.forEach(n => c += ownCost(n)); return c; };
|
||||
/* Preis einer Belegung (D69): Jeder nötige Knoten zählt nur mit dem, was
|
||||
seine Größe über die nötigen Teilpakete HINAUS behauptet (nie negativ) —
|
||||
die Summe bepreist einen Teilbaum so mit seiner Größe, statt
|
||||
Zerlegungstiefe zu bestrafen; gemeinsam Gebrauchtes zählt über die
|
||||
Mengen-Vereinigung weiterhin nur einmal. */
|
||||
const costOf = set => {
|
||||
let c = 0;
|
||||
set.forEach(n => {
|
||||
let kids = 0;
|
||||
for(const k of pathChildren(n)) if(set.has(k)) kids += ownCost(k);
|
||||
c += Math.max(0, ownCost(n) - kids);
|
||||
});
|
||||
return c;
|
||||
};
|
||||
|
||||
let product = 1;
|
||||
for(const grp of coupled){ product *= grp.pool.length; if(product > EXACT_LIMIT) break; }
|
||||
|
||||
@@ -65,10 +65,15 @@ describe('Kosten — Erledigtes zählt nicht mehr', () => {
|
||||
expect(ownCost(a)).toBe(0);
|
||||
});
|
||||
|
||||
it('zieht nur die eigenen Kosten ab, nicht den Teilbaum', () => {
|
||||
it('der erledigte Knoten bepreist 0; sein offenes Kind bleibt auf dem Pfad', () => {
|
||||
/* Seit D69 bepreist die Größe den ganzen Teilbaum — der erledigte
|
||||
Elternknoten kostet nichts mehr, das offene Kind bleibt trotzdem
|
||||
nötig und behält seinen eigenen Preis. */
|
||||
const [n] = roots(`[x] Eltern (L)
|
||||
- [ ] Kind (S)`);
|
||||
expect(cheapestCost(n)).toBe(cheapestCost(roots('[ ] Kind (S)')[0]));
|
||||
expect(cheapestCost(n)).toBe(0);
|
||||
const set = computeCheapSet([n]);
|
||||
expect(set.has(n.children[0])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -48,9 +48,11 @@ describe('Günstigster Pfad — unangetastete Zugaben sind nicht nötig', () =>
|
||||
expect(cheapLabels(BAUM)).toEqual(['Pflicht', 'Wurzel']);
|
||||
});
|
||||
|
||||
it('rechnet die Zugabe nicht in die Kosten des Elternknotens', () => {
|
||||
/* Wurzel (S=2) + Pflicht (S=2) = 4; mit der XXL-Zugabe wären es 10. */
|
||||
expect(cheapestCost(roots(BAUM)[0])).toBe(4);
|
||||
it('die bewertete Wurzel bepreist ihren Teilbaum — die Zugabe erst recht nicht', () => {
|
||||
/* Seit D69 ist der Preis die eigene Größe: Wurzel (S) = 2, unabhängig
|
||||
von Pflicht und XXL-Zugabe. Dass Zugaben auch die GESCHÄTZTE Größe
|
||||
eines größenlosen Knotens nicht erhöhen, sichern die D66-Tests. */
|
||||
expect(cheapestCost(roots(BAUM)[0])).toBe(2);
|
||||
});
|
||||
|
||||
it('vergleicht Alternativen ohne deren Zugaben', () => {
|
||||
@@ -105,9 +107,12 @@ describe('Günstigster Pfad — an einer angefangenen Zugabe wird gearbeitet', (
|
||||
.toEqual(['Teil', 'Wurzel', 'Zugabe']);
|
||||
});
|
||||
|
||||
it('rechnet sie damit auch in die Kosten des Elternknotens', () => {
|
||||
/* Wurzel (S=2) + Zugabe (S=2) = 4; unangetastet wären es 2. */
|
||||
expect(cheapestCost(roots(`[ ] Wurzel (S)\n + [~] Zugabe (S)`)[0])).toBe(4);
|
||||
it('die angefangene Zugabe behält ihren eigenen Preis auf dem Pfad', () => {
|
||||
/* Seit D69 bepreist die Wurzel nur sich selbst (S = 2); die angefangene
|
||||
Zugabe liegt mit ihrem eigenen Preis daneben auf dem Pfad. */
|
||||
const [wurzel] = roots(`[ ] Wurzel (S)\n + [~] Zugabe (S)`);
|
||||
expect(cheapestCost(wurzel)).toBe(2);
|
||||
expect(cheapestCost(wurzel.children[0])).toBe(2);
|
||||
});
|
||||
|
||||
it('macht sie zur Station statt des Elternknotens', () => {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parse } from '../src/parser.js';
|
||||
import { cheapestCost, computeCheapSet, computeCheapPlan } from '../src/model.js';
|
||||
|
||||
/* Die Größe bepreist den ganzen Teilbaum (SPEC §9, D69): Der Preis eines
|
||||
Knotens ist seine angegebene (oder nach D66 geschätzte) Größe — die
|
||||
Teilpakete kommen nicht noch einmal obendrauf. Ob sie hineinpassen, prüft
|
||||
der Größen-Konflikt (§5/D62); die Pfadrechnung zweifelt die Bewertung
|
||||
nicht an. */
|
||||
|
||||
const roots = txt => parse(txt).roots;
|
||||
const cheapLabels = txt => {
|
||||
const r = roots(txt);
|
||||
return [...computeCheapSet(r)].map(n => n.label).sort();
|
||||
};
|
||||
|
||||
describe('Die Größe bepreist den Teilbaum (D69)', () => {
|
||||
it('der gemeldete Fall: die zerlegte S-Alternative schlägt das L', () => {
|
||||
/* Vorher: Manuell = S(2) + XS(1) + S(2) = 5 > Failover L(4) — die
|
||||
sorgfältige Zerlegung wurde bestraft. Jetzt zählt die Bewertung:
|
||||
S(2) < L(4). */
|
||||
const txt = `- kc
|
||||
- Verfügbarkeit
|
||||
| Manuell mit Downtime (S)
|
||||
- Maintainance Mode (XS)
|
||||
- Recovery Plan (S)
|
||||
+ Seite Wartungsarbeiten (S)
|
||||
| Failover (L)
|
||||
| HA (XXL)`;
|
||||
const labels = cheapLabels(txt);
|
||||
expect(labels).toContain('Manuell mit Downtime');
|
||||
expect(labels).not.toContain('Failover');
|
||||
});
|
||||
|
||||
it('der Preis eines bewerteten Knotens ist seine Größe, Kinder egal', () => {
|
||||
const [n] = roots(`- [ ] Paket (S)
|
||||
- [ ] A (XS)
|
||||
- [ ] B (S)`);
|
||||
expect(cheapestCost(n)).toBe(2);
|
||||
});
|
||||
|
||||
it('die Bewertung gilt auch, wenn die Kinder zu groß sind (D62 warnt dann)', () => {
|
||||
/* Vier S unter einem S sind ein sizeConflict — der Fehlermarker meldet
|
||||
es, aber der Knoten bleibt bewertet, wie er bewertet wurde. */
|
||||
const txt = `- [ ] Wahl (XS)
|
||||
| [ ] A (S)
|
||||
- [ ] a1 (S)
|
||||
- [ ] a2 (S)
|
||||
- [ ] a3 (S)
|
||||
- [ ] a4 (S)
|
||||
| [ ] B (M)`;
|
||||
expect(parse(txt).warnings.some(w => w.type === 'sizeConflict')).toBe(true);
|
||||
const labels = cheapLabels(txt);
|
||||
expect(labels).toContain('A');
|
||||
expect(labels).not.toContain('B');
|
||||
});
|
||||
|
||||
it('ohne Größe vertritt die D66-Schätzung die Bewertung', () => {
|
||||
/* Alternative ohne Größe mit einem L-Kind wird als L geschätzt und
|
||||
verliert gegen das bewertete M. */
|
||||
const labels = cheapLabels(`- [ ] Wahl (XS)
|
||||
| [ ] Unbewertet
|
||||
- [ ] Kind (L)
|
||||
| [ ] Bewertet (M)`);
|
||||
expect(labels).toContain('Bewertet');
|
||||
expect(labels).not.toContain('Unbewertet');
|
||||
});
|
||||
|
||||
it('das Vereinigungs-Maß bestraft Zerlegungstiefe nicht (D42/D69)', () => {
|
||||
/* Beide Alternativen ziehen je ein M-schweres Ziel: A ein zerlegtes
|
||||
(M mit zwei S-Kindern), B ein grobes (L). Über die Marginal-Summe
|
||||
wiegen beide 4 — Gleichstand, die erste Alternative (A) gewinnt.
|
||||
Die frühere Knoten-Summe zählte das zerlegte Ziel mit 3+2+2 = 7 und
|
||||
ließe B gewinnen. */
|
||||
const txt = `- [ ] Wurzel (XS)
|
||||
- [ ] Wahl (XS)
|
||||
| [ ] A (S) :#t1
|
||||
| [ ] B (S) :#t2
|
||||
+ [ ] Extras (XS)
|
||||
- [ ] #t1: Zerlegt (M)
|
||||
- [ ] t1a (S)
|
||||
- [ ] t1b (S)
|
||||
- [ ] #t2: Grob (L)`;
|
||||
const { set, exact } = computeCheapPlan(roots(txt));
|
||||
const labels = [...set].map(n => n.label);
|
||||
expect(exact).toBe(true);
|
||||
expect(labels).toContain('A');
|
||||
expect(labels).not.toContain('B');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user