notation: + für optionale Knoten — Zugaben statt Pflicht oder Alternative
Die Notation kannte nur „erforderlich" (-) und „wählbar" (|). Ein einzelnes zusätzliches Feature — weder nötig noch Alternative zu etwas anderem — musste als normales -Kind notiert werden und log damit. Feature-Modelle (FODA) unterscheiden seit den 90ern mandatory/optional/alternative; `+` ergänzt die fehlende zweite Beziehung. Mnemonik: `-` Teilpaket, `+` Zugabe, `|` Alternative. Anlass ist nicht die Optik, sondern der günstigste Pfad (D18): markCheapest() lief bei all-of über ALLE Kinder, jede Zugabe steckte also im errechneten Minimum — systematisch zu groß, und zwar umso mehr, je ehrlicher ein Plan auch die Kür notiert. Sichtbar wird es beim Alternativenvergleich: eine Alternative mit teurer Zugabe verlor gegen eine schlichtere, obwohl die Zugabe gar nicht dazugehört. - Parser setzt `optional:true` und lässt `type:'and'` stehen — `+` gehört zum Knoten, nicht zur Gruppe. Dadurch bleiben gateOf() und die mixedGate-Warnung unverändert richtig: sie meldet weiter genau dann, wenn | mit -/+ gemischt wird. `-` neben `+` ist erlaubt und still — „diese drei sind nötig, das hier wäre schön" ist der Normalfall, nicht der Fehlerfall. - Aus dem Pfad fallen optionale Knoten über pathChildren() heraus, die eine Stelle, die cheapestCost() und markCheapest() gemeinsam nutzen — deshalb wirkt es samt Teilbaum. - Darstellung: hohler Kreis mittig auf der Knotenkante, wo der Abzweig auftrifft (FODA-Konvention). Bewusst KEIN dritter Linienstil: im kompakten Modus codiert allein der Stil das Gate (D15), gepunktet müsste sich dort gegen gestrichelt-grau behaupten. Der Kreis ist orthogonal dazu. CSS-Grundfall ist gestapelt (links/50 %), Ausnahme der horizontale Fächer (oben/50 %), Rück-Ausnahme der gestapelte all-of-Teilbaum unter any-of (D18) — andersherum wären es vier Ausnahmen statt zwei. - SVG-Export zeichnet den Kreis NACH den Knoten (optMarks, Schritt 3a): er liegt halb außerhalb der Box und würde sonst vom Knoten-Rechteck überdeckt. - Legende, Knoten-Tooltip und aria-label in allen neun Sprachen; hint_root formuliert die neue Mischregel. Bekannte Schwäche, bewusst in Kauf genommen: Bei aktivem Pfad-Umschalter wird der optionale Knoten ausgeblasst (opacity:.32) — und mit ihm sein Kreis, der die Erklärung dafür wäre. `opacity` am Elternteil schlägt auf Pseudoelemente durch, das lässt sich nicht zurücknehmen. Das Zurücktreten ist hier die Hauptaussage (wie bei nicht gewählten Alternativen), Tooltip/aria/Legende liefern die Begründung nach. Verhaltensänderung: `+` am Zeilenanfang ist jetzt ein Zeichen und nicht mehr Teil des Labels (`+ 5 % Puffer` ergibt „5 % Puffer"). Test-abgedeckt. SPEC §1/§3/§9/§10 zuerst, dann Code (CLAUDE). Das kanonische Beispiel in §10 enthält jetzt eine `+`-Zeile und ist mit der Test-Fixture wieder deckungsgleich. Der mitgelieferte Werkbaum-Plan markiert Drucklayout, „Was ist neu?" und die Personenfarben als Zugaben — „Was ist neu?" war der Auslöser der Frage. Verifiziert: 12 neue Tests (Parser setzt optional/type; Status/Größe/Tags/URL am +-Knoten; führendes + wird verbraucht; Pfad lässt Zugabe samt Teilbaum aus; Kosten des Elternknotens ohne Zugabe; Alternativenvergleich ohne Zugaben; optionale Knoten bleiben sichtbar; opt-Klasse; keine Warnung bei -/+; Warnung bei |/+; aria-label). Vitest 58/58, Snapshot zeigt `node opt` OHNE `cheap`. Im Browser in allen drei Modi angesehen: Kreis sitzt in horizontal oben mittig, in vertikal und kompakt links auf halber Höhe, jeweils genau auf dem Ende des Abzweigs; SVG-Export enthält beide Kreise an denselben Punkten (gerendert geprüft, nicht nur im Quelltext). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b7637d6393
commit
aa3e087ee6
@@ -16,6 +16,7 @@ const SPEC_EXAMPLE = `%% Projektstruktur – Stand Sprint 14
|
||||
- [~] Umsetzung (XL)
|
||||
- [/] Frontend (S) https://git.example.de/frontend @anna
|
||||
- [ ] Backend (L) @ben @carla
|
||||
+ [?] Dark Mode (S) %% Zugabe, nicht erforderlich
|
||||
- [ ] CMS-Anbindung (M)
|
||||
| [ ] WordPress
|
||||
| [?] Headless CMS
|
||||
@@ -39,11 +40,12 @@ const count = (html, needle) => html.split(needle).length - 1;
|
||||
describe('renderTreeHtml — kanonisches Beispiel', () => {
|
||||
it('Grundzustand (Pfad aus, verworfene aus): Struktur-Snapshot', () => {
|
||||
const {html, warnings} = renderExample();
|
||||
expect(count(html, 'class="node')).toBe(13); // Eigenentwicklung ausgeblendet
|
||||
expect(count(html, 'class="node')).toBe(14); // Eigenentwicklung ausgeblendet
|
||||
expect(count(html, 'cheap-leaf')).toBe(0);
|
||||
expect(count(html, 'size implicit')).toBe(0);
|
||||
expect(count(html, 'ghost-node')).toBe(1); // Backend (L) ist ein M+-Blatt
|
||||
expect(count(html, '<ul class="or">')).toBe(2); // CMS-Anbindung, Hosting
|
||||
expect(html).toMatch(/class="node opt st-idee"[^>]*>Dark Mode/); // `+` (SPEC §3)
|
||||
expect(warnings).toEqual([]);
|
||||
expect(html).toMatchSnapshot();
|
||||
});
|
||||
@@ -57,7 +59,7 @@ describe('renderTreeHtml — kanonisches Beispiel', () => {
|
||||
|
||||
it('verworfene einblenden: Eigenentwicklung erscheint (durchgestrichen)', () => {
|
||||
const {html} = renderExample({showDiscarded: true});
|
||||
expect(count(html, 'class="node')).toBe(14);
|
||||
expect(count(html, 'class="node')).toBe(15);
|
||||
expect(html).toContain('st-verworfen');
|
||||
expect(html).toMatchSnapshot();
|
||||
});
|
||||
@@ -118,15 +120,16 @@ describe('renderTreeHtml — data-line je Knoten (D25)', () => {
|
||||
expect(lineOf(html, 'Website-Relaunch')).toBe(2); // Zeile 1 ist ein %%-Kommentar
|
||||
expect(lineOf(html, 'Zielgruppenanalyse')).toBe(4);
|
||||
expect(lineOf(html, 'Frontend')).toBe(7); // verlinkt -> <a>
|
||||
expect(lineOf(html, 'Headless CMS')).toBe(11); // any-of-Alternative
|
||||
expect(lineOf(html, 'Eigenentwicklung')).toBe(12); // verworfen, eingeblendet
|
||||
expect(lineOf(html, 'Dark Mode')).toBe(9); // optionaler Knoten (+)
|
||||
expect(lineOf(html, 'Headless CMS')).toBe(12); // any-of-Alternative
|
||||
expect(lineOf(html, 'Eigenentwicklung')).toBe(13); // verworfen, eingeblendet
|
||||
});
|
||||
|
||||
it('vergibt jede Zeilennummer genau einmal', () => {
|
||||
const {html} = renderExample({showDiscarded: true});
|
||||
const lines = [...html.matchAll(/class="node[^"]*"[^>]*?data-line="(\d+)"/g)].map(m => m[1]);
|
||||
expect(lines.length).toBe(new Set(lines).size);
|
||||
expect(lines.length).toBe(14); // alle Zeilen des SPEC-Beispiels außer dem Kommentar
|
||||
expect(lines.length).toBe(15); // alle Zeilen des SPEC-Beispiels außer dem Kommentar
|
||||
});
|
||||
|
||||
it('der Geister-Knoten bekommt keine Zeilennummer (er steht für keine Zeile)', () => {
|
||||
|
||||
Reference in New Issue
Block a user