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:
co-authored by
Claude Fable 5
parent
2754fa40f6
commit
d276c94840
@@ -1624,3 +1624,39 @@ der Kandidaten nötig gewesen.
|
||||
nachdenken will (D32).
|
||||
- **`°`** — fehlt auf US-Layouts, und der kleine Kreis kollidierte semantisch
|
||||
mit dem hohlen Kreis, der im Diagramm bereits *optional* bedeutet (D29).
|
||||
|
||||
## D35 — XOR (`=`) umgesetzt: „realisiert“ definiert, „1“-Plakette, keine neue Linienart
|
||||
Das in D34 entschiedene XOR-Gate ist gebaut (SPEC §1/§3/§9); beim Bauen waren
|
||||
drei Dinge zu entscheiden, die die SPEC bis dahin offen ließ:
|
||||
|
||||
**„Realisiert“ heißt: Kosten investiert oder mehr** — Status `[~]`, `[/]`,
|
||||
`[x]`, `[^]`. Die XOR-Regel („genau eine Alternative darf realisiert werden“)
|
||||
soll genau dann anschlagen, wenn der Plan tatsächlich doppelt einkauft — und
|
||||
das beginnt mit `[~]`: Wer an zwei Alternativen zugleich **arbeitet**, verletzt
|
||||
das „genau eine“ bereits, nicht erst beim zweiten `[x]`. `[?]`, `[ ]` und `[!]`
|
||||
sind Absicht ohne Investition, `[-]` ist die Entscheidung dagegen, neutral sagt
|
||||
nichts — alle fünf zählen nicht. Gemeldet wird **jede weitere** realisierte
|
||||
Alternative einzeln (Warnung `xorConflict` mit ihrer Zeilennummer und ihrem
|
||||
Label): Die Meldung zeigt so auf die Zeile, die man ansehen muss, statt
|
||||
pauschal auf die Gruppe. Bewusst eine **Warnung, kein Fehler** — die Notation
|
||||
bleibt fehlertolerant (§4), der Baum wird unverändert dargestellt.
|
||||
|
||||
**Kennzeichnung: „1“-Plakette am Austritt der Sammelleiste.** Der D34-Nachtrag
|
||||
hielt fest, dass `=` optisch bei der konjunktiven Familie steht und das
|
||||
Diagramm die Lesart korrigieren muss. Die Linien übernehmen das schon
|
||||
(gestrichelt grau wie any-of); die Plakette — kleiner weißer Kreis, graue
|
||||
Ziffer, auf dem Leitungsstück zwischen Elternknoten und erstem Abzweig — sagt
|
||||
das „genau eine“, das der Linienstil allein nicht sagen kann. Grau statt
|
||||
Petrol, weil sie zur Gate-Codierung gehört (D15: keine Signalfarbe im
|
||||
Diagramm); an der Sammelleiste statt am Knoten, weil sie eine Aussage über die
|
||||
**Gruppe** ist und die Knoten-Ecken belegt sind (D18). Sie erscheint auch im
|
||||
Grafikexport (dort nach den Knoten gezeichnet, wie der Optional-Kreis aus D29).
|
||||
|
||||
**Keine neue Linienart, keine neue CSS-Familie:** Der Renderer gibt
|
||||
XOR-Gruppen als `<ul class="or xor">` aus — die gesamte any-of-Geometrie
|
||||
(alle drei Modi, D18-Sonderfälle, Export-Routing) gilt damit automatisch;
|
||||
`.xor` ergänzt nur die Plakette. Die Alternative — ein eigener Gruppentyp mit
|
||||
kopierten Regeln — hätte jede künftige Layoutänderung doppelt pflegen lassen.
|
||||
Im Modell ist `'xor'` ein eigener Gate-Wert (`gateOf`), damit die
|
||||
`mixedGate`-Warnung Mischungen mit `|` von selbst meldet; alle
|
||||
Disjunktiv-Abfragen prüfen `!== 'and'`.
|
||||
|
||||
+1
-1
@@ -138,7 +138,7 @@ Pfad, der die Wahrheit sagt.
|
||||
- **XOR** (`=`) — neben „mindestens eine" (`|`) eine Gruppe, in der **genau
|
||||
eine** Alternative realisiert werden darf. Für die Pfadrechnung ändert das
|
||||
nichts (die wählt bei `|` ohnehin eine); es kommt eine Regel hinzu, die
|
||||
verletzt werden kann und dann gemeldet wird.
|
||||
verletzt werden kann und dann gemeldet wird. **Umgesetzt** (SPEC §3, D35).
|
||||
- **Knotenbeschreibungen** — Erläuterungstext zum Knoten, im Diagramm als
|
||||
Tooltip oder Pop-up: kurz direkt beim Knoten, lang als Block am Dokumentende
|
||||
über die ID zugeordnet. Die Arbeit steckt nicht im Anzeigen, sondern in der
|
||||
|
||||
+27
-31
@@ -14,7 +14,8 @@ Alle Bestandteile außer dem Label sind optional. Die Extraktion erfolgt in
|
||||
dieser Reihenfolge (wichtig für Kollisionsfreiheit):
|
||||
|
||||
1. Kommentar entfernen: alles ab `%%` bis Zeilenende.
|
||||
2. Einrückung, Zeichen (`-` / `+` / `|`) und Statusbox `[…]` per Zeilen-Regex.
|
||||
2. Einrückung, Zeichen (`-` / `+` / `|` / `=`) und Statusbox `[…]` per
|
||||
Zeilen-Regex; `=` nur mit folgendem Leerraum (§3).
|
||||
3. URL: erstes Token, das auf `https?://\S+` passt (dadurch stören `@` in URLs nicht).
|
||||
4. Größe: erstes `(XS|S|M|L|XL|XXL)`, Groß-/Kleinschreibung egal.
|
||||
5. Tags: alle `@name`-Vorkommen.
|
||||
@@ -41,7 +42,7 @@ hat etwas, das ein Cursor nicht hat: **alle** sehen dieselbe Stelle.
|
||||
Referenz-Regex der Implementierung:
|
||||
|
||||
```
|
||||
^([ \t]*)([-|+])?\s*(?:\[([ ?~xX^/-])\]\s*)?(.*)$
|
||||
^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:\[([ ?~xX^/-])\]\s*)?(.*)$
|
||||
```
|
||||
|
||||
Für die Fokusmarke (Schritt 6):
|
||||
@@ -55,8 +56,8 @@ Für die Fokusmarke (Schritt 6):
|
||||
- Die Einrückung bestimmt die Ebene. Es gibt keine feste Schrittweite:
|
||||
Elternknoten ist die nächste vorangehende Zeile mit **kleinerer**
|
||||
Einrückungsbreite (Tab zählt als 2 Leerzeichen).
|
||||
- Zeilen ohne Zeichen (`-`/`+`/`|`) sind Wurzelknoten. Mehrere Wurzeln = mehrere
|
||||
Bäume nebeneinander.
|
||||
- Zeilen ohne Zeichen (`-`/`+`/`|`/`=`) sind Wurzelknoten. Mehrere Wurzeln =
|
||||
mehrere Bäume nebeneinander.
|
||||
|
||||
## 3. Zerlegungsart (Gate)
|
||||
|
||||
@@ -65,14 +66,26 @@ Für die Fokusmarke (Schritt 6):
|
||||
| `-` | all of (Und-Zerlegung) | Alle Teilpakete sind erforderlich. |
|
||||
| `+` | optional (Zugabe) | Einzelnes zusätzliches Teilpaket, nicht erforderlich. |
|
||||
| `\|` | any of (Oder-Zerlegung) | Mindestens eine Alternative wird gewählt. |
|
||||
| `=` | exactly one (XOR-Zerlegung) | Genau eine Alternative wird realisiert. |
|
||||
|
||||
- `-` und `|` sind Eigenschaften der **Geschwistergruppe**; `+` ist eine
|
||||
- `-`, `|` und `=` sind Eigenschaften der **Geschwistergruppe**; `+` ist eine
|
||||
Eigenschaft des **einzelnen Knotens** (er hängt an derselben Und-Zerlegung,
|
||||
ist darin aber entbehrlich).
|
||||
- Daraus folgt die Mischregel: Eine Gruppe ist entweder **konjunktiv** — dann
|
||||
dürfen `-` und `+` frei nebeneinander stehen — oder **disjunktiv** (`|`).
|
||||
`|` mit `-`/`+` zu mischen ist ungültig: Darstellung nach dem **ersten** Kind,
|
||||
plus Warnung `mixedGate` mit Zeilennummer.
|
||||
dürfen `-` und `+` frei nebeneinander stehen — oder **disjunktiv**
|
||||
(einheitlich `|` oder einheitlich `=`). Jede andere Mischung ist ungültig:
|
||||
Darstellung nach dem **ersten** Kind, plus Warnung `mixedGate` mit
|
||||
Zeilennummer.
|
||||
- **Leerraum-Regel:** Als Gate wird `=` nur mit **folgendem Leerraum** erkannt —
|
||||
ein Label wie `=SUMME(A1:B2)` bleibt damit ein Label. `=` ist das einzige
|
||||
Gate, das diese Regel braucht; für `-`/`+`/`|` ändert sich nichts.
|
||||
- **XOR-Regel:** In einer `=`-Gruppe darf genau **eine** Alternative realisiert
|
||||
werden. **Realisiert** heißt: Kosten sind investiert oder mehr — Status `[~]`,
|
||||
`[/]`, `[x]` oder `[^]` (§4); `[?]`, `[ ]`, `[!]`, `[-]` und neutrale Knoten
|
||||
zählen nicht. Jede **weitere** realisierte Alternative ergibt eine Warnung
|
||||
`xorConflict` mit ihrer Zeilennummer. Die Regel ist verletzbar, kein
|
||||
Parse-Fehler: Der Baum wird unverändert dargestellt. Für den günstigsten
|
||||
Pfad (§9) verhält sich `=` wie `|` — der wählt ohnehin genau eine.
|
||||
- Ein `+`-Knoten zerlegt sich weiter wie jeder andere; das Gate seiner eigenen
|
||||
Kinder ist davon unabhängig. Optionalität vererbt sich nicht ausdrücklich —
|
||||
wer unter einem `+`-Knoten hängt, ist mit ihm zusammen entbehrlich.
|
||||
@@ -155,6 +168,12 @@ nebeneinander (schmales Diagramm rechts).
|
||||
ist grau (`#6B7A8C`) — kein Petrol mehr im Diagramm. Der Modus ändert nur die
|
||||
**Anordnung**, nicht die Linienfarbe.
|
||||
|
||||
**XOR-Gruppen (`=`, §3)** werden wie any-of gezeichnet (gestrichelt in Grau,
|
||||
gleiche Anordnung). Zusätzlich sitzt am **Austritt der Sammelleiste** — auf dem
|
||||
Stück zwischen Elternknoten und erstem Abzweig — eine kleine **„1“-Plakette**
|
||||
(weißer Kreis mit grauem Rand, graue Ziffer): „genau eine“. Sie erscheint auch
|
||||
im Grafikexport. Siehe D35.
|
||||
|
||||
**Optionale Knoten (`+`, §3):** Sie hängen an der normalen all-of-Zerlegung,
|
||||
die Anordnung bleibt unverändert. Zwei Kennzeichen, beide auch im Grafikexport:
|
||||
|
||||
@@ -596,29 +615,6 @@ die Rechnung schwerer als heute — siehe D34.
|
||||
Zerlegungszeichen — und ob eingeklappte Teilbäume in Grafikexport und Druck
|
||||
eingeklappt bleiben.
|
||||
|
||||
### XOR — genau eine Alternative (`=`)
|
||||
|
||||
Neben `-` (all of), `+` (Zugabe) und `|` (any of, **mindestens** eine) soll es
|
||||
eine Gruppe geben, in der **genau eine** Alternative realisiert werden darf.
|
||||
Zeichen: **`=`**, an derselben Stelle wie die übrigen Zerlegungszeichen
|
||||
(entschieden; Begründung und verworfene Kandidaten: D34-Nachtrag).
|
||||
|
||||
- Für den günstigsten Pfad ändert das nichts: Der wählt bei `|` ohnehin genau
|
||||
eine (§9). XOR fügt eine **Regel** hinzu, die verletzt werden kann — zwei
|
||||
realisierte Alternativen ergeben eine Warnung.
|
||||
- `=` ist **disjunktiv** wie `|`; die Mischregel aus §3 gilt entsprechend:
|
||||
Eine Gruppe ist einheitlich `-`/`+`, einheitlich `|` oder einheitlich `=` —
|
||||
jede Mischung mit `=` ist ungültig (`mixedGate`). Auch die Darstellung folgt
|
||||
any-of (gestrichelt in Grau); wie die Gruppe zusätzlich als „genau eine"
|
||||
gekennzeichnet wird, ist offen (z. B. eine kleine „1" an der Sammelleiste).
|
||||
- **Leerraum-Regel:** Als Gate wird `=` nur mit **folgendem Leerraum**
|
||||
erkannt — ein Label wie `=SUMME(A1:B2)` bleibt damit ein Label. `=` ist das
|
||||
erste Gate, das diese Regel braucht; für `-`/`+`/`|` ändert sich nichts.
|
||||
- Das ursprünglich vorgeschlagene `x` ist verworfen: `x [x] …` stünde als
|
||||
Endzustand jeder entschiedenen Gruppe **dauerhaft** im Text (genau eine
|
||||
Alternative soll ja fertig werden), und als Buchstabe kollidierte es ohne
|
||||
Leerraum-Regel mit gewöhnlichen Labels (`XSS-Schutz` → Gate `X`).
|
||||
|
||||
### Knotenbeschreibungen
|
||||
|
||||
Erläuternder Text zu einem Knoten, im Diagramm als Tooltip oder Pop-up.
|
||||
|
||||
+6
-2
@@ -82,8 +82,12 @@ entscheiden, **bevor** Code entsteht.
|
||||
- [ ] Querverbindungen zeichnen (eigene SVG-Ebene, optisch sekundär);
|
||||
bei ausgewähltem Knoten ein-/ausgehende hervorheben.
|
||||
- [ ] Faltmarken `>` / `<` parsen; interaktives Auf-/Zuklappen im Diagramm.
|
||||
- [ ] XOR-Gruppe (`=`) parsen (nur mit folgendem Leerraum, Mischung →
|
||||
- [x] XOR-Gruppe (`=`) parsen (nur mit folgendem Leerraum, Mischung →
|
||||
`mixedGate`), zwei realisierte Alternativen melden.
|
||||
→ Zeichen entschieden: `=` statt `x` (D34-Nachtrag).
|
||||
→ Zeichen entschieden: `=` statt `x` (D34-Nachtrag). Umgesetzt (D35):
|
||||
„realisiert“ = `[~]`/`[/]`/`[x]`/`[^]`, Warnung `xorConflict` je weiterer
|
||||
realisierter Alternative; Darstellung als any-of plus „1“-Plakette an der
|
||||
Sammelleiste (auch im Grafikexport); Legende + i18n in 9 Sprachen;
|
||||
`tests/xor.test.js`.
|
||||
- [ ] Knotenbeschreibungen: Schreibweise für kurze und lange Form festlegen
|
||||
(Einrückung ist bereits Hierarchie), dann Tooltip/Pop-up.
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
- [?] Effective status: intrinsic, plus what it depends on (S)
|
||||
- [?] Cycles are legal — they mean "finished together" (XS)
|
||||
- [ ] An id with no node behind it is a warning (XS)
|
||||
- [?] XOR, = — exactly one alternative, not at least one (S)
|
||||
- [x] XOR, = — exactly one alternative, not at least one (S)
|
||||
- [?] Node descriptions, shown as a tooltip or pop-up (M)
|
||||
- [?] A short text right at the node (S) %% indentation already means hierarchy
|
||||
- [?] A long block at the end, addressed by its id (S)
|
||||
|
||||
@@ -266,6 +266,14 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
||||
passiert: Zähler stimmte, nichts leuchtete). Vorgehalten wird nur
|
||||
`freshPrevRoots` (Basis, einmal geparst). Basis je Dokument in `werkbaum-seen`,
|
||||
fortgeschrieben **erst beim Bestätigen** über `#freshBtn`.
|
||||
- XOR-Gruppen `=` (SPEC §3/D35): Der Parser setzt `type:'xor'` (nur mit
|
||||
folgendem Leerraum — `=SUMME(…)` bleibt Label); der Renderer gibt
|
||||
`<ul class="or xor">` aus, damit die **gesamte** any-of-Geometrie (alle drei
|
||||
Modi, D18-Sonderfälle, Export-Routing) automatisch gilt — `.xor` ergänzt nur
|
||||
die „1"-Plakette (`ul.xor::after`, im Export `xorMarks`). Disjunktiv-Abfragen
|
||||
auf `gateOf` prüfen `!== 'and'`, nie `=== 'or'`. Die `xorConflict`-Warnung
|
||||
(mehr als eine realisierte Alternative: `[~]`/`[/]`/`[x]`/`[^]`) entsteht im
|
||||
**Parser** (Post-Pass), nicht im Renderer.
|
||||
- Optionale Knoten `+` (D29): Der Parser setzt **`optional:true` und lässt
|
||||
`type:'and'`** — `+` gehört zum Knoten, nicht zur Gruppe. Deshalb bleiben
|
||||
`gateOf()` und die `mixedGate`-Warnung unverändert richtig (sie meldet nur
|
||||
|
||||
+2
-1
@@ -103,7 +103,8 @@
|
||||
<code>+</code> Zugabe, nicht erforderlich <em>(optional)</em><br>
|
||||
<code>!!!</code> hierhin schauen (gemeinsamer Zeigefinger)<br>
|
||||
<code class="or-code">|</code> Alternative, eine wählen <em>(any of)</em><br>
|
||||
Zeile ohne Zeichen = Wurzelknoten. <code class="or-code">|</code> nicht mit <code>-</code>/<code>+</code> mischen.<br>
|
||||
<code class="or-code">=</code> Alternative, genau eine <em>(xor)</em><br>
|
||||
Zeile ohne Zeichen = Wurzelknoten. <code class="or-code">|</code>, <code class="or-code">=</code> und <code>-</code>/<code>+</code> nicht mischen.<br>
|
||||
Status als Kästchen nach dem Zeichen, z. B. <code>- [~] Frontend</code>:
|
||||
<div class="chips">
|
||||
<span class="chip st-idee">[?] Idee</span>
|
||||
|
||||
+41
-9
@@ -288,11 +288,16 @@ function diagramToSvg(){
|
||||
sitzt mittig auf der Kante, das Knoten-Rechteck würde ihn sonst halb
|
||||
überdecken. */
|
||||
const optMarks = [];
|
||||
/* „1"-Plaketten der XOR-Gruppen (`=`, SPEC §9/D35) — wie die Optional-Kreise
|
||||
erst NACH den Knoten gezeichnet, sitzen aber auf der Leitung, nie auf
|
||||
einer Knotenkante. */
|
||||
const xorMarks = [];
|
||||
nodes.forEach(parentEl => {
|
||||
const li = parentEl.closest('li');
|
||||
const childUl = li && [...li.children].find(c => c.tagName === 'UL');
|
||||
if(!childUl) return;
|
||||
const gate = childUl.classList.contains('or') ? 'or' : 'and';
|
||||
const isXor = childUl.classList.contains('xor');
|
||||
const stroke = gate === 'or' ? '#6B7A8C' : '#41556E';
|
||||
const dash = gate === 'or';
|
||||
/* Die Treppe (D29) ist eine Anordnung, keine Ebene: alle Stufen sind Kinder
|
||||
@@ -324,6 +329,7 @@ function diagramToSvg(){
|
||||
const busX = toRight ? Math.min(...kids.map(k=>k.x))-14 : Math.max(...kids.map(k=>k.r))+14;
|
||||
const ys = kids.map(k=>k.cy).concat(p.cy);
|
||||
parts.push(seg(px, p.cy, busX, p.cy, stroke, dash));
|
||||
if(isXor) xorMarks.push({x:(px+busX)/2, y:p.cy});
|
||||
parts.push(seg(busX, Math.min(...ys), busX, Math.max(...ys), stroke, dash));
|
||||
kids.forEach((k, i) => {
|
||||
const x = toRight ? k.x : k.r, o = isOpt(i);
|
||||
@@ -336,6 +342,7 @@ function diagramToSvg(){
|
||||
const busY = toDown ? Math.min(...kids.map(k=>k.y))-14 : Math.max(...kids.map(k=>k.b))+14;
|
||||
const xs = kids.map(k=>k.cx).concat(p.cx);
|
||||
parts.push(seg(p.cx, py, p.cx, busY, stroke, dash));
|
||||
if(isXor) xorMarks.push({x:p.cx, y:(py+busY)/2});
|
||||
parts.push(seg(Math.min(...xs), busY, Math.max(...xs), busY, stroke, dash));
|
||||
kids.forEach((k, i) => {
|
||||
const y = toDown ? k.y : k.b, o = isOpt(i);
|
||||
@@ -393,6 +400,12 @@ function diagramToSvg(){
|
||||
optMarks.forEach(m => parts.push(
|
||||
`<circle cx="${m.x.toFixed(1)}" cy="${m.y.toFixed(1)}" r="4" fill="#ffffff" stroke="#41556E" stroke-width="2"/>`));
|
||||
|
||||
/* 3a′) XOR-Gruppen: „1"-Plakette am Austritt der Sammelleiste (D35) */
|
||||
xorMarks.forEach(m => {
|
||||
parts.push(`<circle cx="${m.x.toFixed(1)}" cy="${m.y.toFixed(1)}" r="6.5" fill="#ffffff" stroke="#6B7A8C" stroke-width="1.5"/>`);
|
||||
parts.push(`<text x="${m.x.toFixed(1)}" y="${(m.y+3).toFixed(1)}" text-anchor="middle" fill="#6B7A8C" font-size="9" font-weight="600">1</text>`);
|
||||
});
|
||||
|
||||
/* 3b) Günstigster-Pfad: abgetönte Kopie über den Knoten + Stationspunkte */
|
||||
if(cheapPts.length >= 2){
|
||||
parts.push(cheapLine('0.2'));
|
||||
@@ -1088,6 +1101,7 @@ const I18N = {
|
||||
zoomAria:"Zoom (Strg/Cmd + Mausrad)", langMore:"weitere Sprachen",
|
||||
empty:"Noch keine Struktur — einfach lostippen.", ghost:"…",
|
||||
mixedWarn:"Zeile {line}: Unter „{label}“ sind - und | gemischt — dargestellt nach dem ersten Kind.",
|
||||
xorConflictWarn:"Zeile {line}: „{label}“ ist eine weitere realisierte Alternative — eine =-Gruppe erlaubt genau eine.",
|
||||
st_idee:"Idee", st_geplant:"geplant", st_arbeit:"in Arbeit", st_durchstich:"Durchstich",
|
||||
st_fertig:"fertig", st_prod:"in Produktion", st_highrisk:"High Risk", st_verworfen:"verworfen",
|
||||
unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.",
|
||||
@@ -1096,9 +1110,10 @@ const I18N = {
|
||||
a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: M (angenommen)", a11yTags:"Zuständig: {names}", a11yOptional:"optional", a11yFocusMark:"hierhin schauen", a11yLink:"verlinkt",
|
||||
hint_indent:"Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.",
|
||||
hint_all:"Teilpaket, alle erforderlich", hint_any:"Alternative, eine wählen",
|
||||
hint_xor:"Alternative, genau eine",
|
||||
hint_opt:"Zugabe, nicht erforderlich",
|
||||
hint_focus:"hierhin schauen (gemeinsamer Zeigefinger)",
|
||||
hint_root:"Zeile ohne Zeichen = Wurzelknoten. | nicht mit - / + mischen.",
|
||||
hint_root:"Zeile ohne Zeichen = Wurzelknoten. |, = und - / + nicht mischen.",
|
||||
hint_status:"Status als Kästchen nach dem Zeichen, z. B.",
|
||||
hint_size:"Aufwand als T-Shirt-Größe in Klammern, Link einfach als URL anhängen:",
|
||||
hint_break:"Ab (M) gilt: weiter untergliedern — fehlt die Untergliederung, erscheint ein Platzhalter im Diagramm.",
|
||||
@@ -1152,6 +1167,7 @@ const I18N = {
|
||||
zoomAria:"Zoom (Ctrl/Cmd + mouse wheel)", langMore:"more languages",
|
||||
empty:"No structure yet — just start typing.", ghost:"…",
|
||||
mixedWarn:"Line {line}: under “{label}”, - and | are mixed — rendered by the first child.",
|
||||
xorConflictWarn:"Line {line}: “{label}” is another realized alternative — an = group allows exactly one.",
|
||||
st_idee:"idea", st_geplant:"planned", st_arbeit:"in progress", st_durchstich:"walking skeleton",
|
||||
st_fertig:"done", st_prod:"in production", st_highrisk:"high risk", st_verworfen:"discarded",
|
||||
unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.",
|
||||
@@ -1160,9 +1176,10 @@ const I18N = {
|
||||
a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: M (assumed)", a11yTags:"Assigned: {names}", a11yOptional:"optional", a11yFocusMark:"look here", a11yLink:"has link",
|
||||
hint_indent:"Indentation (2 spaces or a tab) defines the hierarchy.",
|
||||
hint_all:"sub-task, all required", hint_any:"alternative, choose one",
|
||||
hint_xor:"alternative, exactly one",
|
||||
hint_opt:"extra, not required",
|
||||
hint_focus:"look here (a shared pointer)",
|
||||
hint_root:"Line without a marker = root node. Do not mix | with - / +.",
|
||||
hint_root:"Line without a marker = root node. Do not mix |, = and - / +.",
|
||||
hint_status:"Status as a checkbox after the marker, e.g.",
|
||||
hint_size:"Effort as a T-shirt size in parentheses; add a link simply as a URL:",
|
||||
hint_break:"From (M) on: break it down further — if the breakdown is missing, a placeholder appears in the diagram.",
|
||||
@@ -1216,6 +1233,7 @@ const I18N = {
|
||||
zoomAria:"Zoom (Ctrl/Cmd + rueda del ratón)", langMore:"más idiomas",
|
||||
empty:"Aún no hay estructura — simplemente empieza a escribir.", ghost:"…",
|
||||
mixedWarn:"Línea {line}: bajo «{label}» se mezclan - y | — se representa según el primer hijo.",
|
||||
xorConflictWarn:"Línea {line}: «{label}» es otra alternativa realizada — un grupo = permite exactamente una.",
|
||||
st_idee:"idea", st_geplant:"planificado", st_arbeit:"en curso", st_durchstich:"prototipo funcional",
|
||||
st_fertig:"terminado", st_prod:"en producción", st_highrisk:"alto riesgo", st_verworfen:"descartado",
|
||||
unknownStatusWarn:"Línea {line}: código de estado desconocido «{code}» — mostrado como neutral.",
|
||||
@@ -1224,9 +1242,10 @@ const I18N = {
|
||||
a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: M (asumido)", a11yTags:"Responsable: {names}", a11yOptional:"opcional", a11yFocusMark:"mirar aquí", a11yLink:"con enlace",
|
||||
hint_indent:"La sangría (2 espacios o un tabulador) define la jerarquía.",
|
||||
hint_all:"subtarea, todas obligatorias", hint_any:"alternativa, elige una",
|
||||
hint_xor:"alternativa, exactamente una",
|
||||
hint_opt:"extra, no obligatorio",
|
||||
hint_focus:"mirar aquí (un puntero compartido)",
|
||||
hint_root:"Línea sin marcador = nodo raíz. No mezcles | con - / +.",
|
||||
hint_root:"Línea sin marcador = nodo raíz. No mezcles |, = y - / +.",
|
||||
hint_status:"Estado como casilla tras el marcador, p. ej.",
|
||||
hint_size:"Esfuerzo como talla de camiseta entre paréntesis; añade un enlace simplemente como URL:",
|
||||
hint_break:"A partir de (M): sigue desglosando — si falta el desglose, aparece un marcador de posición en el diagrama.",
|
||||
@@ -1280,6 +1299,7 @@ const I18N = {
|
||||
zoomAria:"Zoom (Ctrl/Cmd + molette)", langMore:"plus de langues",
|
||||
empty:"Pas encore de structure — commencez à taper.", ghost:"…",
|
||||
mixedWarn:"Ligne {line} : sous « {label} », - et | sont mélangés — rendu selon le premier enfant.",
|
||||
xorConflictWarn:"Ligne {line} : « {label} » est une alternative réalisée de plus — un groupe = n’en autorise qu’une seule.",
|
||||
st_idee:"idée", st_geplant:"planifié", st_arbeit:"en cours", st_durchstich:"squelette fonctionnel",
|
||||
st_fertig:"terminé", st_prod:"en production", st_highrisk:"risque élevé", st_verworfen:"abandonné",
|
||||
unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.",
|
||||
@@ -1288,9 +1308,10 @@ const I18N = {
|
||||
a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : M (supposé)", a11yTags:"Responsable : {names}", a11yOptional:"facultatif", a11yFocusMark:"regarder ici", a11yLink:"avec lien",
|
||||
hint_indent:"L'indentation (2 espaces ou une tabulation) définit la hiérarchie.",
|
||||
hint_all:"sous-tâche, toutes requises", hint_any:"alternative, en choisir une",
|
||||
hint_xor:"alternative, exactement une",
|
||||
hint_opt:"supplément, non requis",
|
||||
hint_focus:"regarder ici (un pointeur partagé)",
|
||||
hint_root:"Ligne sans marqueur = nœud racine. Ne mélangez pas | avec - / +.",
|
||||
hint_root:"Ligne sans marqueur = nœud racine. Ne mélangez pas |, = et - / +.",
|
||||
hint_status:"Statut sous forme de case après le marqueur, p. ex.",
|
||||
hint_size:"Effort en taille de T-shirt entre parenthèses ; ajoutez un lien simplement comme URL :",
|
||||
hint_break:"À partir de (M) : décomposer davantage — si la décomposition manque, un espace réservé apparaît dans le diagramme.",
|
||||
@@ -1344,6 +1365,7 @@ const I18N = {
|
||||
zoomAria:"Powiększenie (Ctrl/Cmd + kółko myszy)", langMore:"więcej języków",
|
||||
empty:"Brak struktury — zacznij pisać.", ghost:"…",
|
||||
mixedWarn:"Wiersz {line}: pod „{label}” mieszają się - i | — renderowane według pierwszego dziecka.",
|
||||
xorConflictWarn:"Wiersz {line}: „{label}” to kolejna zrealizowana alternatywa — grupa = dopuszcza dokładnie jedną.",
|
||||
st_idee:"pomysł", st_geplant:"zaplanowane", st_arbeit:"w toku", st_durchstich:"działający szkielet",
|
||||
st_fertig:"gotowe", st_prod:"w produkcji", st_highrisk:"wysokie ryzyko", st_verworfen:"odrzucone",
|
||||
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
|
||||
@@ -1352,9 +1374,10 @@ const I18N = {
|
||||
a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: M (założony)", a11yTags:"Przypisano: {names}", a11yOptional:"opcjonalny", a11yFocusMark:"spójrz tutaj", a11yLink:"z linkiem",
|
||||
hint_indent:"Wcięcie (2 spacje lub tabulator) definiuje hierarchię.",
|
||||
hint_all:"podzadanie, wszystkie wymagane", hint_any:"alternatywa, wybierz jedną",
|
||||
hint_xor:"alternatywa, dokładnie jedna",
|
||||
hint_opt:"dodatek, niewymagany",
|
||||
hint_focus:"spójrz tutaj (wspólny wskaźnik)",
|
||||
hint_root:"Wiersz bez znacznika = węzeł główny. Nie mieszaj | z - / +.",
|
||||
hint_root:"Wiersz bez znacznika = węzeł główny. Nie mieszaj |, = i - / +.",
|
||||
hint_status:"Status jako pole wyboru po znaczniku, np.",
|
||||
hint_size:"Nakład jako rozmiar koszulki w nawiasach; link dodaj po prostu jako URL:",
|
||||
hint_break:"Od (M): dziel dalej — gdy brakuje podziału, w diagramie pojawia się symbol zastępczy.",
|
||||
@@ -1408,6 +1431,7 @@ const I18N = {
|
||||
zoomAria:"Масштаб (Ctrl/Cmd + колесо мыши)", langMore:"ещё языки",
|
||||
empty:"Пока нет структуры — просто начните печатать.", ghost:"…",
|
||||
mixedWarn:"Строка {line}: под «{label}» смешаны - и | — отображается по первому потомку.",
|
||||
xorConflictWarn:"Строка {line}: «{label}» — ещё одна реализованная альтернатива, а группа = допускает ровно одну.",
|
||||
st_idee:"идея", st_geplant:"запланировано", st_arbeit:"в работе", st_durchstich:"сквозной прототип",
|
||||
st_fertig:"готово", st_prod:"в эксплуатации", st_highrisk:"высокий риск", st_verworfen:"отклонено",
|
||||
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
|
||||
@@ -1416,9 +1440,10 @@ const I18N = {
|
||||
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: M (предполагается)", a11yTags:"Ответственные: {names}", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой",
|
||||
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
|
||||
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
|
||||
hint_xor:"альтернатива, ровно одна",
|
||||
hint_opt:"дополнение, не обязательно",
|
||||
hint_focus:"смотрите здесь (общая указка)",
|
||||
hint_root:"Строка без маркера = корневой узел. Не смешивайте | с - / +.",
|
||||
hint_root:"Строка без маркера = корневой узел. Не смешивайте |, = и - / +.",
|
||||
hint_status:"Статус в виде флажка после маркера, напр.",
|
||||
hint_size:"Трудоёмкость как размер футболки в скобках; ссылку добавьте просто как URL:",
|
||||
hint_break:"С (M): дробите дальше — если декомпозиции нет, в диаграмме появляется заполнитель.",
|
||||
@@ -1472,6 +1497,7 @@ const I18N = {
|
||||
zoomAria:"ज़ूम (Ctrl/Cmd + माउस-व्हील)", langMore:"और भाषाएँ",
|
||||
empty:"अभी कोई संरचना नहीं — बस टाइप करना शुरू करें।", ghost:"…",
|
||||
mixedWarn:"पंक्ति {line}: „{label}“ के अंतर्गत - और | मिश्रित हैं — पहले चाइल्ड के अनुसार दिखाया गया।",
|
||||
xorConflictWarn:"पंक्ति {line}: „{label}“ एक और साकार विकल्प है — = समूह में केवल एक की अनुमति है।",
|
||||
st_idee:"विचार", st_geplant:"नियोजित", st_arbeit:"प्रगति पर", st_durchstich:"कार्यशील ढाँचा",
|
||||
st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_highrisk:"उच्च जोखिम", st_verworfen:"अस्वीकृत",
|
||||
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
|
||||
@@ -1480,9 +1506,10 @@ const I18N = {
|
||||
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: M (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित",
|
||||
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
|
||||
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
|
||||
hint_xor:"विकल्प, ठीक एक",
|
||||
hint_opt:"अतिरिक्त, आवश्यक नहीं",
|
||||
hint_focus:"यहाँ देखें (साझा संकेतक)",
|
||||
hint_root:"बिना मार्कर वाली पंक्ति = मूल नोड। | को - / + के साथ न मिलाएँ।",
|
||||
hint_root:"बिना मार्कर वाली पंक्ति = मूल नोड। |, = और - / + को आपस में न मिलाएँ।",
|
||||
hint_status:"मार्कर के बाद चेकबॉक्स के रूप में स्थिति, जैसे",
|
||||
hint_size:"प्रयास कोष्ठक में टी-शर्ट आकार के रूप में; लिंक बस URL के रूप में जोड़ें:",
|
||||
hint_break:"(M) से आगे: और विभाजित करें — विभाजन न होने पर आरेख में प्लेसहोल्डर दिखता है।",
|
||||
@@ -1536,6 +1563,7 @@ const I18N = {
|
||||
zoomAria:"缩放(Ctrl/Cmd + 鼠标滚轮)", langMore:"更多语言",
|
||||
empty:"还没有结构——直接开始输入吧。", ghost:"…",
|
||||
mixedWarn:"第 {line} 行:在「{label}」下 - 与 | 混用——按第一个子项渲染。",
|
||||
xorConflictWarn:"第 {line} 行:「{label}」是又一个已实现的备选项——= 组只允许恰好一个。",
|
||||
st_idee:"想法", st_geplant:"已计划", st_arbeit:"进行中", st_durchstich:"可运行骨架",
|
||||
st_fertig:"已完成", st_prod:"已上线", st_highrisk:"高风险", st_verworfen:"已放弃",
|
||||
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
|
||||
@@ -1544,9 +1572,10 @@ const I18N = {
|
||||
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:M(假定)", a11yTags:"负责人:{names}", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接",
|
||||
hint_indent:"缩进(2 个空格或制表符)定义层级。",
|
||||
hint_all:"子任务,全部必需", hint_any:"备选项,择其一",
|
||||
hint_xor:"备选项,恰好一个",
|
||||
hint_opt:"附加项,非必需",
|
||||
hint_focus:"看这里(共享的指针)",
|
||||
hint_root:"无标记的行 = 根节点。请勿将 | 与 - / + 混用。",
|
||||
hint_root:"无标记的行 = 根节点。请勿混用 |、= 与 - / +。",
|
||||
hint_status:"在标记后用方框表示状态,例如",
|
||||
hint_size:"用括号中的 T 恤尺码表示工作量;链接直接作为 URL 附加:",
|
||||
hint_break:"从 (M) 起:继续细分——若缺少细分,图表中会出现占位符。",
|
||||
@@ -1600,6 +1629,7 @@ const I18N = {
|
||||
zoomAria:"ズーム(Ctrl/Cmd + マウスホイール)", langMore:"その他の言語",
|
||||
empty:"まだ構造がありません — 入力を始めてください。", ghost:"…",
|
||||
mixedWarn:"{line} 行目:「{label}」の下で - と | が混在 — 最初の子に従って表示。",
|
||||
xorConflictWarn:"{line} 行目:「{label}」も実現済みの選択肢です — = グループで実現できるのは 1 つだけです。",
|
||||
st_idee:"アイデア", st_geplant:"計画済み", st_arbeit:"作業中", st_durchstich:"ウォーキングスケルトン",
|
||||
st_fertig:"完了", st_prod:"本番稼働", st_highrisk:"高リスク", st_verworfen:"破棄",
|
||||
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
|
||||
@@ -1608,9 +1638,10 @@ const I18N = {
|
||||
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: M(想定)", a11yTags:"担当: {names}", a11yOptional:"任意", a11yFocusMark:"ここを見る", a11yLink:"リンクあり",
|
||||
hint_indent:"インデント(スペース2つまたはタブ)で階層を定義します。",
|
||||
hint_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ",
|
||||
hint_xor:"選択肢、ちょうど1つ",
|
||||
hint_opt:"追加、必須ではない",
|
||||
hint_focus:"ここを見る(共有の指さし)",
|
||||
hint_root:"マーカーのない行 = ルートノード。| を - / + と混在させないでください。",
|
||||
hint_root:"マーカーのない行 = ルートノード。|・=・- / + を混在させないでください。",
|
||||
hint_status:"マーカーの後にチェックボックスで状態、例:",
|
||||
hint_size:"工数は括弧内の T シャツサイズで;リンクは URL としてそのまま追加:",
|
||||
hint_break:"(M) 以上:さらに分解 — 分解がないと図にプレースホルダーが表示されます。",
|
||||
@@ -1631,6 +1662,7 @@ function buildHint(){
|
||||
<code>-</code> ${esc(t('hint_all'))} <em>(all of)</em><br>
|
||||
<code>+</code> ${esc(t('hint_opt'))} <em>(optional)</em><br>
|
||||
<code class="or-code">|</code> ${esc(t('hint_any'))} <em>(any of)</em><br>
|
||||
<code class="or-code">=</code> ${esc(t('hint_xor'))} <em>(xor)</em><br>
|
||||
${esc(t('hint_root'))}<br>
|
||||
${esc(t('hint_status'))} <code>- [~] Frontend</code>:
|
||||
<div class="chips">
|
||||
|
||||
+10
-6
@@ -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);
|
||||
|
||||
+31
-6
@@ -19,14 +19,19 @@ export const STATUS_BY_CODE = {
|
||||
'!': {key:'highrisk', name:'High Risk – Aufwand unklar'}
|
||||
};
|
||||
|
||||
/* Status, die als „realisiert" zählen (XOR-Regel, SPEC §3/D35): Kosten sind
|
||||
investiert oder mehr. Absicht (`[?]`, `[ ]`, `[!]`), Ablehnung (`[-]`) und
|
||||
neutrale Knoten zählen nicht. */
|
||||
const REALIZED = new Set(['arbeit', 'durchstich', 'fertig', 'prod']);
|
||||
|
||||
/* Parst den Notationstext zu { roots, warnings }.
|
||||
Jeder Knoten: {label, type:'and'|'or', optional, status, url, size, tags,
|
||||
focus, children, line}.
|
||||
Jeder Knoten: {label, type:'and'|'or'|'xor', optional, status, url, size,
|
||||
tags, focus, children, line}.
|
||||
`type` ist das Gate der Geschwistergruppe, `optional` (Zeichen `+`, SPEC §3)
|
||||
eine Eigenschaft des einzelnen Knotens: er hängt an derselben Und-Zerlegung
|
||||
(`type:'and'`), ist darin aber entbehrlich. Dadurch bleibt die
|
||||
Gemischt-Warnung unverändert richtig — sie schlägt nur an, wenn `|` mit
|
||||
`-`/`+` gemischt wird.
|
||||
Gemischt-Warnung unverändert richtig — sie schlägt an, wenn `|` oder `=`
|
||||
mit `-`/`+` (oder untereinander) gemischt wird.
|
||||
Extraktionsreihenfolge (SPEC §1): Kommentar -> Zeichen/Status -> URL -> Größe
|
||||
-> Tags -> Fokusmarke -> Label. Hierarchie über Einrückungsbreite (Tab = 2 Leerzeichen);
|
||||
Elternknoten ist die nächste vorangehende Zeile mit kleinerer Breite. */
|
||||
@@ -41,9 +46,11 @@ export function parse(text){
|
||||
/* Statusbox tolerant erfassen: irgendein einzelnes Zeichen in [ ] an der
|
||||
Statusposition. Gültige Codes -> Status; unbekannte -> Warnung + neutral
|
||||
(fehlertolerant: die Zeile geht nicht verloren). */
|
||||
const m = raw.match(/^([ \t]*)([-|+])?\s*(?:\[([^\]])\]\s*)?(.*)$/);
|
||||
/* `=` (XOR, SPEC §3) nur mit folgendem Leerraum — die Leerraum-Regel hält
|
||||
Labels wie `=SUMME(A1:B2)` heraus; `-`/`+`/`|` bleiben wie bisher. */
|
||||
const m = raw.match(/^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:\[([^\]])\]\s*)?(.*)$/);
|
||||
const width = m[1].replace(/\t/g,' ').length;
|
||||
const type = m[2] === '|' ? 'or' : 'and';
|
||||
const type = m[2] === '|' ? 'or' : m[2] === '=' ? 'xor' : 'and';
|
||||
const optional = m[2] === '+';
|
||||
const boxChar = m[3]; // undefined, wenn keine Statusbox
|
||||
|
||||
@@ -74,5 +81,23 @@ export function parse(text){
|
||||
stack.push({node, width});
|
||||
});
|
||||
|
||||
/* XOR-Regel (SPEC §3): In einer `=`-Gruppe darf genau EINE Alternative
|
||||
realisiert sein. Jede weitere wird einzeln gemeldet — die Warnung zeigt so
|
||||
auf die Zeile, die man ansehen muss, statt pauschal auf die Gruppe (D35).
|
||||
Gruppen-Gate nach dem ersten Kind, wie in der Darstellung (§3). */
|
||||
(function checkXor(node){
|
||||
const kids = node.children;
|
||||
if(kids.length && kids[0].type === 'xor'){
|
||||
let realized = 0;
|
||||
for(const k of kids){
|
||||
if(k.status && REALIZED.has(k.status.key)){
|
||||
realized++;
|
||||
if(realized > 1) warnings.push({type:'xorConflict', line:k.line, label:k.label});
|
||||
}
|
||||
}
|
||||
}
|
||||
kids.forEach(checkXor);
|
||||
})(virtualRoot);
|
||||
|
||||
return {roots: virtualRoot.children, warnings};
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ function extraCls(n, opts){
|
||||
Abzweigs). Leere Liste ⇒ gar kein Attribut. */
|
||||
function liClass(visibleKids, opts, optional){
|
||||
const cls = [
|
||||
visibleKids.length ? (gateOf(visibleKids) === 'or' ? 'has-or' : 'has-and') : '',
|
||||
visibleKids.length ? (gateOf(visibleKids) !== 'and' ? 'has-or' : 'has-and') : '',
|
||||
optional ? 'opt' : ''
|
||||
].filter(Boolean);
|
||||
return cls.length ? ` class="${cls.join(' ')}"` : '';
|
||||
@@ -103,14 +103,18 @@ function renderChildren(node, warnings, opts){
|
||||
const kids = visibleChildren(node, opts.showDiscarded);
|
||||
if(!kids.length) return '';
|
||||
/* Gemischte Gates (SPEC §3): Da `+` nur `optional` setzt und `type:'and'`
|
||||
behält, schlägt das hier weiterhin genau dann an, wenn `|` mit `-`/`+`
|
||||
gemischt wird — `-` neben `+` ist erlaubt und still. */
|
||||
behält, schlägt das hier genau dann an, wenn `|` oder `=` mit `-`/`+`
|
||||
(oder untereinander) gemischt wird — `-` neben `+` ist erlaubt und still. */
|
||||
const types = new Set(kids.map(k => k.type));
|
||||
if(types.size > 1){
|
||||
/* strukturierte Warnung (Typ + Zeile); Formatierung in warnings.js */
|
||||
warnings.push({type: 'mixedGate', line: kids[0].line, label: node.label});
|
||||
}
|
||||
const gate = gateOf(kids);
|
||||
/* XOR-Gruppen (`=`, SPEC §3/§9) erben die komplette any-of-Geometrie über
|
||||
die Klasse `or` (alle Modi, Export-Routing); `xor` ergänzt nur die
|
||||
„1"-Plakette an der Sammelleiste (D35). */
|
||||
const ulCls = gate === 'xor' ? 'or xor' : gate;
|
||||
const items = kids.map(k => {
|
||||
const vk = visibleChildren(k, opts.showDiscarded);
|
||||
/* `opt` auch am <li>: den Abzweig zeichnen dessen Pseudoelemente, er wird
|
||||
@@ -121,7 +125,7 @@ function renderChildren(node, warnings, opts){
|
||||
renderChildren(k, warnings, opts) +
|
||||
`</li>`;
|
||||
}).join('');
|
||||
return `<ul class="${gate}">${items}</ul>`;
|
||||
return `<ul class="${ulCls}">${items}</ul>`;
|
||||
}
|
||||
|
||||
/* Baut den inneren HTML-String für #out aus (bereits gefilterten) Wurzeln und
|
||||
|
||||
@@ -722,6 +722,20 @@
|
||||
ul.or::before{
|
||||
content:'';position:absolute;top:0;left:9px;height:14px;border-left:2px dashed var(--muted);
|
||||
}
|
||||
/* XOR (`=`, SPEC §3/§9, D35): Geometrie kommt komplett von `ul.or` (der
|
||||
Renderer gibt `class="or xor"` aus); `.xor` ergänzt nur die „1"-Plakette
|
||||
am Austritt der Sammelleiste — „genau eine". Grau wie die any-of-Führung
|
||||
(Gate-Codierung, keine Signalfarbe, D15); sitzt mittig auf dem
|
||||
14-px-Einlaufstück (left:9px = Leisten-x in allen Modi). */
|
||||
ul.xor::after{
|
||||
content:'1';
|
||||
position:absolute;left:9px;top:7px;transform:translate(-50%,-50%);
|
||||
width:13px;height:13px;border-radius:50%;
|
||||
background:var(--card);border:1.5px solid var(--muted);
|
||||
box-sizing:border-box;
|
||||
color:var(--muted);font-size:9px;font-weight:600;line-height:10px;
|
||||
text-align:center;
|
||||
}
|
||||
ul.or>li{position:relative;align-items:flex-start;padding:5px 0 5px 30px}
|
||||
/* Abzweig auf Höhe der Knoten-Mitte (5px li-Padding + halbe Knotenhöhe), nicht Mitte des Teilbaums */
|
||||
ul.or>li::before{
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
Bekannte Typen:
|
||||
- mixedGate { line, label } — Geschwister mit gemischtem Gate (SPEC §3)
|
||||
- unknownStatus { line, code } — unbekanntes Statuszeichen (Phase 2)
|
||||
- xorConflict { line, label } — weitere realisierte Alternative in einer
|
||||
`=`-Gruppe (SPEC §3/D35); je Zeile eine
|
||||
Warnung, damit sie dorthin zeigt
|
||||
- sourceLoad { url, error } — ?sourceUrl= nicht ladbar (D23); ohne
|
||||
Zeilennummer, erscheint dadurch zuoberst
|
||||
- padRateLimit { seconds } — zu früh nachgeladen; Werkbaum hat gar nicht
|
||||
@@ -29,6 +32,8 @@ export function formatWarning(w, t){
|
||||
return t('mixedWarn', {line: w.line, label: esc(w.label)});
|
||||
case 'unknownStatus':
|
||||
return t('unknownStatusWarn', {line: w.line, code: esc(w.code)});
|
||||
case 'xorConflict':
|
||||
return t('xorConflictWarn', {line: w.line, label: esc(w.label)});
|
||||
case 'sourceLoad':
|
||||
return t('sourceLoadWarn', {url: esc(w.url), error: esc(w.error)});
|
||||
case 'padRateLimit':
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parse } from '../src/parser.js';
|
||||
import { gateOf, computeCheapSet } from '../src/model.js';
|
||||
import { renderTreeHtml } from '../src/render.js';
|
||||
|
||||
const t = key => key;
|
||||
const roots = txt => parse(txt).roots;
|
||||
const render = txt => renderTreeHtml(roots(txt),
|
||||
{t, showDiscarded: false, cheapPath: false, cheapSet: new Set()});
|
||||
const cheapLabels = txt => [...computeCheapSet(roots(txt))].map(n => n.label).sort();
|
||||
|
||||
/* XOR-Gruppen: `=` (SPEC §3, D34/D35). Disjunktiv wie `|`, aber genau EINE
|
||||
Alternative darf realisiert werden. */
|
||||
describe('Parser — `=` als XOR-Gate mit Leerraum-Regel', () => {
|
||||
it('erkennt `=` mit folgendem Leerraum als Gate', () => {
|
||||
const [wurzel] = roots(`[ ] Wahl\n = [ ] A\n = [ ] B`);
|
||||
expect(wurzel.children.map(k => [k.label, k.type]))
|
||||
.toEqual([['A', 'xor'], ['B', 'xor']]);
|
||||
});
|
||||
|
||||
it('lässt `=` ohne folgenden Leerraum im Label (Leerraum-Regel)', () => {
|
||||
const [wurzel] = roots(`[ ] Wurzel\n - =SUMME(A1:B2)`);
|
||||
expect(wurzel.children.map(k => [k.label, k.type]))
|
||||
.toEqual([['=SUMME(A1:B2)', 'and']]);
|
||||
});
|
||||
|
||||
it('parst Status, Größe, Tags und URL am `=`-Knoten wie sonst auch', () => {
|
||||
const [wurzel] = roots(`[ ] Wahl\n = [~] A (M) https://example.org/a @ana`);
|
||||
const k = wurzel.children[0];
|
||||
expect([k.label, k.type, k.status.key, k.size, k.tags, k.url])
|
||||
.toEqual(['A', 'xor', 'arbeit', 'M', ['ana'], 'https://example.org/a']);
|
||||
});
|
||||
|
||||
it('meldet gateOf für eine XOR-Gruppe als eigenen Wert', () => {
|
||||
const [wurzel] = roots(`[ ] Wahl\n = [ ] A\n = [ ] B`);
|
||||
expect(gateOf(wurzel.children)).toBe('xor');
|
||||
});
|
||||
});
|
||||
|
||||
describe('XOR-Regel — genau eine Alternative darf realisiert sein', () => {
|
||||
it('warnt nicht bei null oder einer realisierten Alternative', () => {
|
||||
expect(parse(`[ ] Wahl\n = [ ] A\n = [ ] B`).warnings).toEqual([]);
|
||||
expect(parse(`[ ] Wahl\n = [x] A\n = [ ] B`).warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it('meldet jede WEITERE realisierte Alternative mit ihrer Zeile', () => {
|
||||
const {warnings} = parse(`[ ] Wahl\n = [x] A\n = [~] B\n = [/] C`);
|
||||
expect(warnings).toEqual([
|
||||
{type: 'xorConflict', line: 3, label: 'B'},
|
||||
{type: 'xorConflict', line: 4, label: 'C'}
|
||||
]);
|
||||
});
|
||||
|
||||
it('zählt schon `[~]` als realisiert — Kosten sind investiert', () => {
|
||||
const {warnings} = parse(`[ ] Wahl\n = [~] A\n = [~] B`);
|
||||
expect(warnings).toEqual([{type: 'xorConflict', line: 3, label: 'B'}]);
|
||||
});
|
||||
|
||||
it('zählt Absicht, Ablehnung und neutrale Knoten nicht als realisiert', () => {
|
||||
/* [?], [ ], [!], [-] und ohne Statusbox — keine davon ist realisiert. */
|
||||
const {warnings} = parse(
|
||||
`[ ] Wahl\n = [^] A\n = [?] B\n = [ ] C\n = [!] D\n = [-] E\n = F`);
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it('prüft verschachtelte XOR-Gruppen unabhängig voneinander', () => {
|
||||
const {warnings} = parse(`[ ] Wurzel
|
||||
- [ ] Teil
|
||||
= [x] A
|
||||
= [x] B
|
||||
- [ ] Anderes
|
||||
= [ ] C
|
||||
= [x] D`);
|
||||
expect(warnings).toEqual([{type: 'xorConflict', line: 4, label: 'B'}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mischregel — `=` ist disjunktiv, jede Mischung warnt', () => {
|
||||
it('warnt, wenn `=` mit `|` gemischt wird', () => {
|
||||
const {warnings} = render(`[ ] Wahl\n = [ ] A\n | [ ] B`);
|
||||
expect(warnings).toEqual([{type: 'mixedGate', line: 2, label: 'Wahl'}]);
|
||||
});
|
||||
|
||||
it('warnt, wenn `=` mit `-` oder `+` gemischt wird', () => {
|
||||
const {warnings} = render(`[ ] Wahl\n = [ ] A\n + [ ] B`);
|
||||
expect(warnings).toEqual([{type: 'mixedGate', line: 2, label: 'Wahl'}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Darstellung und günstigster Pfad', () => {
|
||||
it('rendert die XOR-Gruppe als `ul.or.xor` (any-of-Geometrie + Plakette)', () => {
|
||||
const {html} = render(`[ ] Wahl\n = [ ] A\n = [ ] B`);
|
||||
expect(html).toContain('<ul class="or xor">');
|
||||
expect(html).toContain('<li class="has-or">');
|
||||
});
|
||||
|
||||
it('wählt im günstigsten Pfad die günstigste XOR-Alternative', () => {
|
||||
expect(cheapLabels(`[ ] Wahl (XS)\n = [ ] A (L)\n = [ ] B (S)`))
|
||||
.toEqual(['B', 'Wahl']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user