From 59da485af6cf5b28d734b4bb1a38a75d88a439fa Mon Sep 17 00:00:00 2001 From: mhoennig Date: Tue, 25 Aug 2026 10:19:52 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Gr=C3=B6=C3=9Fe=20ist=20das=20letzte=20?= =?UTF-8?q?alleinstehende=20`(L)`-Token=20der=20Zeile=20(D68)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Erkannt nur alleinstehend angesetzt (wie #id und :#…) — damit greifen die Zitier-Konventionen "(L)" und ((L)) von selbst, und Backend(L) bleibt Label. Das letzte Token gewinnt statt des ersten: Die Größe steht hinter dem Titel, frühere Vorkommen sind Text. SPEC §1/§5 und llms.md nachgezogen; mitgelieferte Beispiele unverändert (Parse-Vergleich alt/neu). Co-Authored-By: Claude Fable 5 --- docs/CHANGELOG.md | 1 + docs/DECISIONS.md | 42 +++++++++++++++++++++++++++++++++++ docs/SPEC.md | 26 +++++++++++++++++++++- frontend/public/llms.md | 10 ++++++--- frontend/src/parser.js | 16 ++++++++++++- frontend/tests/parser.test.js | 29 ++++++++++++++++++++++++ 6 files changed, 119 insertions(+), 5 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f0e9dbc..229f2b6 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -19,6 +19,7 @@ reverse. ## 2026-08-25 +- The size is now the last free-standing `(L)`-style token of the line — earlier ones stay in the title, and `"(L)"` or `((L))` mention a size literally - Fix: Ctrl+click on a dependency preceded by a non-breaking space did nothing, although the parser reads it as a dependency - Ctrl+click a dependency `:#id` in the text jumps to the line that defines the ID — Ctrl+Enter does the same at the caret diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index b0ae962..3c82e8b 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -5056,3 +5056,45 @@ kann den Handler prinzipiell nicht treffen — der erste Prüflauf sah deshalb wie ein Fehler aus, der keiner war. Geprüft wird das Tastatur-Pendant mit einem korrekt gebauten `KeyboardEvent`; der Klick-Weg ließ sich dagegen echt auslösen. + +## D68 — Größe: das letzte alleinstehende Token, nicht das erste +Gemeldet vom Nutzer: Manchmal braucht der Titel runde Klammern — und wenn ihr +Inhalt zufällig ein Größenkürzel ist (`Variante (L) bauen`), fraß die +Extraktion das Titel-`(L)` als Größe. Mit echter Angabe dahinter war es +doppelt falsch: Das Literal wurde die Größe, das gemeinte `(M)` blieb im +Label. Entschieden (Nutzer): beide vorgeschlagenen Regeln kombiniert. + +**Alleinstehend angesetzt** (`(^|\s)\(…\)`) — dieselbe Regel-Familie wie bei +`#id`, `:#…`, `!!!` und `&tag`, und der eigentliche Gewinn liegt darin, dass +die **vorhandenen Zitier-Konventionen dadurch von selbst greifen**: `"(L)"` +bleibt Label (das `(` hängt am `"`), `((L))` ebenso (am äußeren `(`). Kein +neues Zeichen, kein Escape — die Notation escapt weiterhin nirgends (D59). +Wer eine Größe erwähnen will, zitiert sie; llms.md führt beide Formen in der +Zitier-Faustregel. + +**Das letzte Token gewinnt, nicht das erste.** Die übliche Schreibweise +stellt die Größe hinter den Titel — das letzte Token ist die Angabe, alles +davor ist Text. Damit löst sich auch der Fall ohne Anführungszeichen richtig +auf: `Variante (L) bauen (M)` → Größe `M`, Label „Variante (L) bauen“. In +der Idee-Runde war „letztes statt erstes“ zunächst verworfen worden („stille +Umdeutung bestehender Zeilen“); der Einwand wiegt hier wenig, weil die alte +Erste-gewinnt-Lesart in genau diesen Zeilen schon falsch war — es gibt keine +richtige Bedeutung, die verloren ginge. + +**Preis, benannt:** `Backend(L)` ohne Leerzeichen ist keine Größe mehr. Der +Fehlermodus ist der laute (das Badge fehlt sichtbar, D59-Haltung), und in den +mitgelieferten Beispielen kommt die enge Schreibweise nicht vor — geprüft per +Grep und per Parse-Vergleich alt/neu über alle `docs/examples/*.werkbaum` +(Knoten- und Größenzahlen identisch, Warnungen unverändert; die drei +`sizeConflict` der Demo-Pläne sind Altbestand). + +**Umgesetzt als Schleife über alle Treffer** statt eines cleveren +Rückwärts-Regex: gut lesbar, und der führende Leerraum der Fundstelle bleibt +beim Entfernen stehen (wie `pre` bei den übrigen Extraktionen). SPEC §1 +(Schritt 4, eigener Größen-Block, Referenz-Regex) und §5 zuerst, llms.md im +selben Zug (Schritt-3-Regel, Größen-Abschnitt, Zitier-Faustregel). + +**Nachgemessen:** 445 Tests (4 neue in `tests/parser.test.js`); Gegenprobe +per Mutation: erster statt letzter Treffer → genau der Letztes-gewinnt-Test +fällt; Anker entfernt → genau die zwei Alleinstehend-/Zitier-Tests. Alle +Snapshots (kanonisches Beispiel §10) unverändert. diff --git a/docs/SPEC.md b/docs/SPEC.md index 30d320c..cccdc42 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -20,7 +20,9 @@ dieser Reihenfolge (wichtig für Kollisionsfreiheit): (`>` / `<`) per Zeilen-Regex; `=` nur mit folgendem Leerraum (§3), die Faltmarke ebenso (siehe unten). 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. +4. Größe: das **letzte** alleinstehend angesetzte `(XS|S|M|L|XL|XXL)`, + Groß-/Kleinschreibung egal (siehe unten). Frühere Vorkommen bleiben im + Label stehen. 5. Tags: alle `@name`-Vorkommen. 6. Knoten-ID: das **erste** alleinstehend angesetzte `#name`-Token (siehe unten). 7. Abhängigkeiten: alle alleinstehend angesetzten `:#a,#b`-Token (siehe unten). @@ -84,6 +86,19 @@ Referenz-Regex (Schritt 1b, geprüft auf der kommentarfreien Zeile): Bild sagen dasselbe. Sie sagen nichts über Fortschritt (§4) oder Notwendigkeit (§3) und ändern weder Kosten noch Warnungen. +**Größe `(L)`** — die T-Shirt-Größe (§5) im Zeilenformat: + +- Erkannt nur **alleinstehend angesetzt** (`(^|\s)\(…\)`, dieselbe Regel wie + bei `#name` und `:#…`): `Backend(L)` bleibt damit ein Label, und die + Zitier-Konventionen gelten auch hier — `"(L)"` und `((L))` bleiben Label, + wer eine Größe **erwähnen** will, schreibt sie so. +- Das **letzte** solche Token der Zeile ist die Größe; frühere Vorkommen + bleiben im Label stehen (`Variante (L) bauen (M)` → Größe `M`, Label + „Variante (L) bauen“). Die Größe steht nach der üblichen Schreibweise + hinter dem Titel — das letzte Token ist die Angabe, alles davor ist Text. + (Bis D68 galt das **erste** Token; die Umkehrung trifft nur Zeilen mit + mehreren Kandidaten.) + **Knoten-ID `#name`** — benennt einen Knoten im **ganzen Dokument** eindeutig; sie ist die Adresse für Abhängigkeiten und Beschreibungsblöcke (§11). @@ -201,6 +216,13 @@ sie setzt die Marke immer in die zweite Stellung und löst die erste dabei auf: ^([ \t]*(?:[-|+]|=(?=[ \t]))?[ \t]*)(?:[><](?=[ \t])[ \t]*)?((?:\[[^\]]\][ \t]*)?)(?:[><](?=[ \t])[ \t]*)? ``` +Für die Größe (Schritt 4, nur der **letzte** Treffer; der führende Leerraum +bleibt stehen): + +``` +(^|\s)\((XXL|XS|XL|S|M|L)\) +``` + Für die Knoten-ID (Schritt 6, nur der erste Treffer; die letzte Gruppe ist der optionale Trenn-Doppelpunkt, der mit entfällt): @@ -313,6 +335,8 @@ effektiv nicht weiter sein als das, was er braucht. ## 5. Aufwand (T-Shirt-Größen) - Werte: `XS < S < M < L < XL < XXL`, notiert in Klammern, z. B. `(L)`. + Erkannt wird das letzte alleinstehend angesetzte Token der Zeile (§1) — + `"(L)"` und `((L))` im Titel bleiben Text. - **Untergliederungsregel:** Ab `(M)` muss ein Element weiter zerlegt sein. Ein Element ≥ M **ohne Kinder** erhält einen Geister-Knoten an gestrichelter Linie darunter (in `--warn`, `#B45309`). Sein Label ist knapp „…“; die diff --git a/frontend/public/llms.md b/frontend/public/llms.md index c2bf240..92c1973 100644 --- a/frontend/public/llms.md +++ b/frontend/public/llms.md @@ -105,7 +105,10 @@ One node per line. Everything except the label is optional. ### Size (effort) -- T-shirt sizes in parentheses: `(XS) (S) (M) (L) (XL) (XXL)`. +- T-shirt sizes in parentheses: `(XS) (S) (M) (L) (XL) (XXL)`. Recognized + only **free-standing** (preceded by start-of-line or whitespace), and the + **last** such token of the line is the size — earlier ones stay in the + label. `"(L)"` and `((L))` mention a size literally (quoting convention). - From `(M)` upward a node **should be decomposed further**; a node ≥ M without children gets a placeholder hint in the diagram. - For cost estimation a missing size is **estimated from the sub-packages**: @@ -134,7 +137,8 @@ One node per line. Everything except the label is optional. (whole line or trailing part). This also applies inside descriptions. 2. Bare `https?://…` URL — makes the node clickable; extracted early so `@`, `#` and `!!!` inside URLs never trigger. -3. `(SIZE)` — first match, case-insensitive. +3. `(SIZE)` — the **last free-standing** match, case-insensitive; earlier + size-like tokens stay in the label. 4. `@name` — people tags; several per line, any position. Characters: Unicode letters, digits, `.`, `_`, `-`. 5. `#id` — **node ID**: the *first* free-standing `#token` of the line @@ -214,7 +218,7 @@ One node per line. Everything except the label is optional. - Keep rejected alternatives as `[-]` instead of deleting them: the decision stays visible (add the reason as a `%%` comment). - To mention syntax literally in a label without triggering it, wrap it in - parentheses or quotes: `(#id)`, `(:#a,#b)`, `"#123"`. + parentheses or quotes: `(#id)`, `(:#a,#b)`, `"#123"`, `"(L)"`, `((L))`. ## Complete example diff --git a/frontend/src/parser.js b/frontend/src/parser.js index f2a62bf..4d977a0 100644 --- a/frontend/src/parser.js +++ b/frontend/src/parser.js @@ -286,7 +286,21 @@ export function parse(text){ let rest = m[6], url = null, size = null; const tags = []; rest = rest.replace(/https?:\/\/\S+/i, s => { url = s; return ''; }); - rest = rest.replace(/\((XXL|XS|XL|S|M|L)\)/i, (s, g) => { size = g.toUpperCase(); return ''; }); + /* Größe (SPEC §1 Schritt 4, D68): das LETZTE alleinstehend angesetzte + `(L)`-Token der Zeile — die Größe steht nach der üblichen Schreibweise + hinter dem Titel, frühere Vorkommen sind Text und bleiben im Label. + Alleinstehend wie bei `#id` und `:#…`: `Backend(L)` bleibt Label, und + die Zitier-Konventionen `"(L)"` und `((L))` gelten damit von selbst. */ + { + const reSize = /(^|\s)\((XXL|XS|XL|S|M|L)\)/gi; + let sm, lastSize = null; + while((sm = reSize.exec(rest))) lastSize = sm; + if(lastSize){ + size = lastSize[2].toUpperCase(); + rest = rest.slice(0, lastSize.index) + lastSize[1] + + rest.slice(lastSize.index + lastSize[0].length); + } + } rest = rest.replace(/@([\p{L}\p{N}._-]+)/gu, (s, g) => { tags.push(g); return ''; }); /* Knoten-ID `#name` (SPEC §1, D36): nur ALLEINSTEHEND ANGESETZT — „C#" bleibt Label, und das reservierte `:#a,#b` (§11) wird nicht gefressen. diff --git a/frontend/tests/parser.test.js b/frontend/tests/parser.test.js index efec8c0..435b874 100644 --- a/frontend/tests/parser.test.js +++ b/frontend/tests/parser.test.js @@ -128,6 +128,35 @@ describe('parse — Randfälle', () => { expect(roots[1].size).toBe('M'); }); + /* Größen-Token (SPEC §1 Schritt 4, D68): das LETZTE alleinstehend + angesetzte `(L)`-Token ist die Größe; frühere bleiben im Label, und die + Zitier-Konventionen `"(L)"`/`((L))` halten ein Literal heraus. */ + it('das letzte alleinstehende Größen-Token gewinnt, frühere bleiben im Label', () => { + const { roots } = parse('- Variante (L) bauen (M)'); + expect(roots[0].size).toBe('M'); + expect(roots[0].label).toBe('Variante (L) bauen'); + }); + + it('Größe nur alleinstehend angesetzt: Backend(L) bleibt Label', () => { + const { roots } = parse('- Backend(L)'); + expect(roots[0].size).toBe(null); + expect(roots[0].label).toBe('Backend(L)'); + }); + + it('Zitier-Konventionen: "(L)" und ((L)) bleiben Label', () => { + const { roots } = parse('- Merch "(L)" drucken\n- Auswahl ((L)) treffen (S)'); + expect(roots[0].size).toBe(null); + expect(roots[0].label).toBe('Merch "(L)" drucken'); + expect(roots[1].size).toBe('S'); + expect(roots[1].label).toBe('Auswahl ((L)) treffen'); + }); + + it('Größe am Zeilenanfang des Rests zählt weiterhin', () => { + const { roots } = parse('- (L) Backend'); + expect(roots[0].size).toBe('L'); + expect(roots[0].label).toBe('Backend'); + }); + it('ohne Angaben: status/url/size null, tags leer', () => { const { roots } = parse('- Nackt'); expect(roots[0]).toMatchObject({ status: null, url: null, size: null, tags: [] });