fix: Erledigte Knoten treten nie zurück (D46-Nachtrag)

Gemeldet: manche Knoten in Produktion werden grau dargestellt, etwa
`#ed.fresh`. Ursache war nicht der Status, sondern die Pfad-Inversion:
Der Knoten ist eine Zugabe (`+`) und liegt damit per D29 nie auf dem
günstigsten Pfad — `.cheap-on .node:not(.cheap)` blasste ihn aus
(Deckkraft 0,32, Sättigung 0,4; Pastellblau liest sich so als Grau).

D46 hatte „erledigt tritt nicht zurück" bereits entschieden, die Regel
hing aber an `cheapCls()` und griff nur für Knoten AUF dem Pfad. Durch
das Raster fielen die fertige Zugabe und die fertige, nicht gewählte
Alternative. Jetzt gilt sie unabhängig vom Pfad; Blässe heißt damit
einheitlich „hier ist nichts getan und wird auch nichts getan".

Umgesetzt als eigene Klasse `done` per `isDone(n)` in render.js, nicht
über `st-fertig`/`st-prod`: Die Statusklasse trägt den EFFEKTIVEN Status
(D39), die Ausnahme muss dem intrinsischen folgen — dieselbe Linie wie
bei D35, D28 und der Kostenrechnung aus D46. Gefärbt wird unverändert
nach dem effektiven Status.

Nachgemessen (Werkbaum-Plan, 149 sichtbare Knoten, Pfad an): vorher
blass und fertig waren genau drei Knoten (#ed.fresh, #ed.export.print,
#ed.closure.greedy) — jetzt alle drei opacity 1, filter none, Füllung
rgb(219,234,248) wie die fertigen Knoten auf dem Pfad. Weiterhin blass:
31 Knoten, darunter kein einziger fertiger. Der Grafikexport hatte den
Fehler nie (er liest backgroundColor, nicht opacity).

240 Tests grün (4 neue in tests/frontier.test.js).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-24 06:15:22 +02:00
co-authored by Claude Opus 5
parent 4619ce7a0b
commit b6e83b4f2a
8 changed files with 147 additions and 10 deletions
+48
View File
@@ -3,6 +3,7 @@ import { parse } from '../src/parser.js';
import {
isDone, ownCost, cheapestCost, cheapCls, computeCheapSet,
} from '../src/model.js';
import { renderTreeHtml } from '../src/render.js';
/* Status-bewusster günstigster Pfad (SPEC §9, D46): Erledigtes kostet nichts
mehr und ist keine Station; hervorgehoben wird der günstigste noch OFFENE
@@ -144,3 +145,50 @@ describe('Eingeklappt — der Knoten vertritt offene Arbeit, nicht erledigte', (
expect(cheapCls(zweig, set, true)).toBe('cheap'); /* auf dem Pfad, aber nichts zu tun */
});
});
/* Die Klasse `done` trägt allein die Ausnahme von der Pfad-Inversion
(D46-Nachtrag): Erledigtes wird nie ausgeblasst, auch wenn es nicht auf
dem Pfad liegt. Sie folgt dem INTRINSISCHEN Status. */
describe('Klasse `done` — Erledigtes tritt nie zurück', () => {
const html = txt => {
const rs = roots(txt);
return renderTreeHtml(rs, {t: k => k, showDiscarded: false,
cheapPath: true, cheapSet: computeCheapSet(rs)}).html;
};
it('setzt sie an [x] und [^], nicht an offenen Knoten', () => {
const h = html(`[ ] W (XS)
- [x] Fertig (S)
- [^] Live (S)
- [~] Offen (S)`);
expect(h).toContain('class="node cheap done st-fertig"');
expect(h).toContain('class="node cheap done st-prod"');
expect(h).toContain('class="node cheap cheap-leaf st-arbeit"');
});
it('setzt sie auch am fertigen OPTIONALEN Knoten, der nie auf dem Pfad liegt', () => {
const rs = roots(`[ ] W (XS)
- [ ] Pflicht (S)
+ [^] Zugabe (S)`);
const set = computeCheapSet(rs);
const zugabe = rs[0].children[1];
expect(set.has(zugabe)).toBe(false); /* `+` ist nie auf dem Pfad (D29) */
const h = renderTreeHtml(rs, {t: k => k, showDiscarded: false,
cheapPath: true, cheapSet: set}).html;
expect(h).toContain('class="node opt done st-prod"');
});
it('setzt sie an der fertigen, nicht gewählten Alternative', () => {
const h = html(`[ ] W (XS)
| [^] Alt A (XS)
| [^] Alt B (XS)`);
expect(h.match(/class="node[^"]*done[^"]*st-prod"/g)).toHaveLength(2);
});
it('setzt sie nicht am verworfenen Knoten', () => {
const h = html(`[ ] W (XS)
- [ ] A (S)
- [-] Weg (S)`);
expect(h).not.toContain('done st-verworfen');
});
});