fix: eingeklappter Knoten vertritt seinen Teilbaum auf dem günstigsten Pfad
D38 hielt fest, ein eingeklappter Zweig dürfe Pfadknoten verbergen, „die Inversion an den sichtbaren Knoten bleibt richtig". Zu kurz gedacht: Die LINIE blieb eben nicht richtig. Sie fädelt durch die cheap-leaf-Knoten im DOM; ein eingeklappter Pfadknoten ist zwar `cheap`, aber kein Blatt (seine Pfad-Kinder existieren im Modell) — also bekam er keine Station, und die Linie umging den ganzen Zweig. Im Beispiel nachgemessen: „Concept" war `node cheap folded`, verbarg zwei Pfadknoten, trug keinen Punkt; die Linie begann erst bei „PWA". Das Bild behauptete damit, in diesem Zweig sei nichts zu tun — eine Aussage über den Plan, nicht über die Ansicht. Genau die Grenze, die D38 sonst zieht. Regel: Eingeklappt überspringt cheapCls() die Blatt-Prüfung und fragt den Teilbaum (hidesCheap). Liegt darin etwas auf dem Pfad, ist der Knoten dessen tiefste noch sichtbare Station. Beim Aufklappen geben die Kinder sie zurück — gemessen 6 -> 7 -> 6 Stationen beim Hin- und Herklappen. Auch ohne eigene Pfad-Mitgliedschaft: Ein per :#… gezogenes Ziel kann unter einem +-Knoten liegen, der selbst nicht gebraucht wird (D42). Eingeklappt ist dieser der einzige sichtbare Griff auf nötige Arbeit — er wird Station und zählt als cheap, tritt also nicht zurück. Ihn auszublassen wäre die Lüge. 4 neue Tests (208 gesamt). SPEC §9, D38 (Nachtrag) und frontend/CLAUDE.md nachgezogen; die alte „hinnehmbar"-Stelle in D38 ist als falsch markiert statt gelöscht. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6463a76e1e
commit
38104a4c5d
@@ -126,3 +126,64 @@ describe('Renderer — eingeklappte Teilbäume', () => {
|
||||
.toEqual(['A', 'A2', 'W']);
|
||||
});
|
||||
});
|
||||
|
||||
/* Der eingeklappte Knoten vertritt seinen Teilbaum auch auf dem günstigsten
|
||||
Pfad (SPEC §9, D38-Nachtrag): Sonst überspränge die Pfad-Linie den ganzen
|
||||
Zweig, als wäre dort nichts zu tun. */
|
||||
describe('Günstigster Pfad an eingeklappten Knoten', () => {
|
||||
/* Nimmt die BEREITS geparsten Wurzeln: `collapsedSet` und `cheapSet` prüfen
|
||||
auf Objektidentität — ein zweiter Parse-Durchlauf liefert andere Objekte
|
||||
und die Mengen träfen nie zu (dieselbe Falle wie in D28). */
|
||||
const cheapRender = (r, collapsedSet) =>
|
||||
renderTreeHtml(r, {t, showDiscarded: false, cheapPath: true,
|
||||
cheapSet: computeCheapSet(r), collapsedSet: collapsedSet || new Set()}).html;
|
||||
/* Labels der Stationen (`cheap-leaf`) in Dokumentreihenfolge — genau die
|
||||
Menge, durch die drawCheapPath() die Linie fädelt. Gelesen aus dem
|
||||
`aria-label` (erstes Glied = Label): Im Knoteninneren steht bei
|
||||
eingeklappten Knoten das Falt-Zeichen vor dem Text. */
|
||||
const stations = html =>
|
||||
[...html.matchAll(/class="node[^"]*cheap-leaf[^"]*"[^>]*aria-label="([^",]*)/g)]
|
||||
.map(m => m[1]);
|
||||
|
||||
const TXT = `[ ] W (XS)\n - [ ] A (S)\n - [ ] A1 (S)\n - [ ] A2 (S)\n - [ ] B (S)`;
|
||||
|
||||
it('macht den eingeklappten Knoten zur Station statt den Zweig zu überspringen', () => {
|
||||
const r = roots(TXT);
|
||||
const a = r[0].children[0];
|
||||
expect(stations(cheapRender(r))).toEqual(['A1', 'A2', 'B']); /* offen */
|
||||
expect(stations(cheapRender(r, new Set([a])))).toEqual(['A', 'B']); /* zu */
|
||||
});
|
||||
|
||||
it('gibt die Station beim Aufklappen wieder an die Kinder ab', () => {
|
||||
const r = roots(TXT);
|
||||
const a = r[0].children[0];
|
||||
const zu = stations(cheapRender(r, new Set([a])));
|
||||
const auf = stations(cheapRender(r, new Set()));
|
||||
expect(zu).not.toContain('A1');
|
||||
expect(auf).toContain('A1');
|
||||
expect(auf).not.toContain('A');
|
||||
});
|
||||
|
||||
it('vertritt auch einen Teilbaum, den der Knoten selbst nicht braucht', () => {
|
||||
/* `+ O` ist entbehrlich und nicht auf dem Pfad — sein Kind wird aber per
|
||||
Abhängigkeit gezogen (D42). Eingeklappt ist `O` der einzige sichtbare
|
||||
Griff darauf und darf deshalb weder fehlen noch ausgeblasst sein. */
|
||||
const txt = `[ ] W (XS)\n - [ ] N (S) :#t\n + [ ] O (S)\n - [ ] #t: T (S)`;
|
||||
const r = roots(txt);
|
||||
const o = r[0].children[1];
|
||||
const set = computeCheapSet(r);
|
||||
expect([...set].map(n => n.label).sort()).toEqual(['N', 'T', 'W']); /* O selbst nicht */
|
||||
const {html} = renderTreeHtml(r, {t, showDiscarded: false, cheapPath: true,
|
||||
cheapSet: set, collapsedSet: new Set([o])});
|
||||
expect(stations(html)).toContain('O');
|
||||
});
|
||||
|
||||
it('macht einen eingeklappten Knoten ohne Pfad im Teilbaum NICHT zur Station', () => {
|
||||
const txt = `[ ] W (XS)\n - [ ] N (S)\n + [ ] O (S)\n - [ ] P (S)`;
|
||||
const r = roots(txt);
|
||||
const o = r[0].children[1];
|
||||
const {html} = renderTreeHtml(r, {t, showDiscarded: false, cheapPath: true,
|
||||
cheapSet: computeCheapSet(r), collapsedSet: new Set([o])});
|
||||
expect(stations(html)).toEqual(['N']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user