feat: XOR-Gruppen (=) — genau eine Alternative (SPEC §3, D35)

Parser erkennt `=` als disjunktives Gate (nur mit folgendem Leerraum —
`=SUMME(…)` bleibt Label); Mischungen melden mixedGate wie gehabt. Die
XOR-Regel warnt je WEITERER realisierter Alternative (xorConflict mit
Zeilennummer; realisiert = [~]/[/]/[x]/[^], siehe D35). Darstellung erbt
die komplette any-of-Geometrie (ul class="or xor"), ergänzt um eine
„1"-Plakette am Austritt der Sammelleiste — auch im Grafikexport.
Legende (+ hint_root) und Warntext in allen 9 Sprachen; SPEC §11-Eintrag
in §1/§3/§9 überführt; 13 neue Tests (tests/xor.test.js).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-22 16:08:37 +02:00
co-authored by Claude Fable 5
parent 2754fa40f6
commit d276c94840
14 changed files with 291 additions and 61 deletions
+10 -6
View File
@@ -5,10 +5,14 @@
import { SIZE_RANK } from './parser.js';
/* Gate der Geschwistergruppe: 'or', wenn das erste Kind '|' trägt, sonst 'and'
(SPEC §3 — Darstellung nach dem ersten Kind). */
/* Gate der Geschwistergruppe nach dem ERSTEN Kind (SPEC §3): 'or' (`|`),
'xor' (`=`) oder 'and' (`-`/`+`). 'xor' bleibt ein eigener Wert, damit die
mixedGate-Warnung Mischungen mit `|` meldet; wer nur konjunktiv/disjunktiv
unterscheidet, prüft `!== 'and'` (D35). */
export function gateOf(children){
return children.length && children[0].type === 'or' ? 'or' : 'and';
if(!children.length) return 'and';
const t = children[0].type;
return t === 'or' || t === 'xor' ? t : 'and';
}
/* Untergliederungspflicht ab Größe M ohne Kinder (SPEC §5); verworfene nie. */
@@ -25,7 +29,7 @@ export function visibleChildren(n, showDiscarded){
/* ---------- Günstigster Pfad (D18) ----------
Nötige Knoten für die günstigste Realisierung: all-of ⇒ alle Kinder,
any-of ⇒ nur die günstigste Alternative. „Günstig" = kleinste rekursive
any-of und XOR ⇒ nur die günstigste Alternative. „Günstig" = kleinste rekursive
Kosten (eigene Größe + Kinder; any-of das Minimum). Verworfene zählen nie
mit (unabhängig vom Einblenden-Toggle). Gleichstand ⇒ erste. Fehlende
Größe = M.
@@ -43,7 +47,7 @@ export function cheapestCost(n){
const kids = pathChildren(n);
let c = ownCost(n);
if(kids.length){
if(gateOf(kids) === 'or') c += Math.min(...kids.map(cheapestCost));
if(gateOf(kids) !== 'and') c += Math.min(...kids.map(cheapestCost));
else c += kids.reduce((s, k) => s + cheapestCost(k), 0);
}
return c;
@@ -52,7 +56,7 @@ export function markCheapest(n, set){
set.add(n);
const kids = pathChildren(n);
if(!kids.length) return;
if(gateOf(kids) === 'or'){
if(gateOf(kids) !== 'and'){
let best = null, bc = Infinity;
for(const k of kids){ const c = cheapestCost(k); if(c < bc){ bc = c; best = k; } }
if(best) markCheapest(best, set);