From 3018db9f6139efdc4ab4b0a115735ef90c2b74a1 Mon Sep 17 00:00:00 2001 From: mhoennig Date: Mon, 24 Aug 2026 13:31:39 +0200 Subject: [PATCH] =?UTF-8?q?notation:=20ohne=20Titel=20vertritt=20die=20Kno?= =?UTF-8?q?ten-ID=20ihn=20(SPEC=20=C2=A71,=20D60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `- #US-123` ergab bisher nichts — leeres Label, Zeile ignoriert, ID nicht vergeben. Wo die Kennung schon der Name ist (Ticket-Referenzen, §11), war das verkehrt herum: Wer den Titel nicht danebenschrieb, verlor den ganzen Knoten. Jetzt wird `#id` das Label, mit Doppelpunkt geschrieben wie ohne. Das `#` bleibt im Label — es sagt „hier steht die Adresse, weil es keinen Titel gibt". Der `#`-Umschalter setzt bei so einem Knoten nichts davor (sonst `#US-123: #US-123`), und Tooltip wie aria-label lassen die ID weg, die schon im Titel steht. Verhaltensänderung: Die ID ist damit vergeben — `- #auth` gefolgt von `- [ ] Echt #auth` gibt jetzt zwei Knoten und eine duplicateId-Warnung. SPEC §1, llms.md, D60; 6 neue Tests, der alte auf die neue Regel umgeschrieben. Co-Authored-By: Claude Opus 5 --- docs/CHANGELOG.md | 1 + docs/DECISIONS.md | 44 ++++++++++++++++++++++++++++++++++++++ docs/SPEC.md | 15 ++++++++++--- frontend/public/llms.md | 7 +++++- frontend/src/parser.js | 10 +++++++-- frontend/src/render.js | 10 ++++++--- frontend/tests/ids.test.js | 34 ++++++++++++++++++++++++++--- 7 files changed, 109 insertions(+), 12 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 53fae72..f3a3ab5 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -19,6 +19,7 @@ reverse. ## 2026-08-24 +- A node written as an id alone (`- #US-123`) is now titled by that id, instead of being ignored - A line ending in a space and `\` continues on the next line, so a long node no longer has to fit into one - A star in the header opens What's new: the changes of the last few days, and a link per day that shows the nodes it touched in the diagram - A warning triangle replaces the question mark as the pointer over faulty line numbers diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 481b552..ea7c094 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -4275,3 +4275,47 @@ Titel (L) @anna` ergibt einen Knoten mit `data-line="2"` und 0 Warnungen; der Cursor auf Zeile 3 hebt denselben Knoten hervor wie auf Zeile 2. 20 neue Tests; die Gegenprobe (Leerraum-Pflicht aus dem Regex entfernt) lässt genau die zwei danach benannten Zusicherungen fallen. + +## D60 — Ohne Titel vertritt die Knoten-ID ihn +D36 hielt fest: „Eine Zeile, die **nur** aus einer ID besteht, wird wie jede +leere Zeile ignoriert und belegt die ID nicht." Das ist jetzt umgekehrt — eine +solche Zeile ist ein Knoten, und sein Label ist `#id`. + +**Der Anlass ist die Ticket-Referenz.** §11 hält fest, dass die Kennung eines +Trackers oft die natürliche Knoten-ID ist (`#US-123`, `#ABC-123`). Wo das +zutrifft, **ist die Kennung schon der Name** — `- #US-123: US-123` daneben zu +schreiben wäre eine Verdopplung, und wer sie wegließe, verlor bisher den +ganzen Knoten. Die alte Regel war für den Fall gedacht, dass jemand eine ID +ohne Absicht stehen lässt; sie hat dabei den häufigeren Fall miterschlagen. + +**Das Label ist `#id`, mit Doppelkreuz.** Erwogen war die ID ohne `#` +(`US-123`) oder eine eigene, zurückgenommene Darstellung wie beim +`#`-Umschalter (mono, grau, D56). Beides verworfen: Ohne `#` liest sich der +Knoten wie ein gewöhnlicher Titel, der zufällig nach einer Kennung aussieht — +das `#` sagt „hier steht die Adresse, weil es keinen Titel gibt". Und eine +durchgehend graue Beschriftung ließe den Knoten wie zurückgetreten aussehen, +was er nicht ist. + +**Der `#`-Umschalter setzt bei so einem Knoten nichts davor.** Sonst stünde +dort `#US-123: #US-123`. Erkannt wird das an `labelFromId` am Knoten und nicht +am Vergleich `label === '#' + id`: Wer `#a: #a` bewusst schreibt, hat einen +Titel, und der soll auch mit Umschalter so erscheinen. Aus demselben Grund +entfallen für solche Knoten die ID-Zeile im Tooltip und `a11yId` im +`aria-label` — ein Screenreader läse die Kennung sonst zweimal hintereinander. + +**Die ID ist damit vergeben.** Das ist die eigentliche Verhaltensänderung und +die einzige, die jemandem auffallen kann: `- #auth` gefolgt von +`- [ ] Echt #auth` gibt jetzt zwei Knoten und eine `duplicateId`-Warnung, wo +vorher einer und keine Warnung stand. Das ist richtig herum — die erste Zeile +ist jetzt ein Knoten, und zwei Knoten mit derselben ID sind genau der Fall, +für den es die Warnung gibt. + +Eine Zeile **ohne** ID und ohne Label bleibt, was sie war: keine Zeile. Auch +`- (L) @anna` ergibt weiterhin nichts — Größe und Zuständige allein sind kein +Knoten. + +**Nachgemessen:** `- [x] #US-123 (L) @anna` ergibt einen Knoten mit Label +`#US-123`, Größe `L`, Tag `anna`, Status fertig; mit eingeschaltetem +`#`-Umschalter bekommt er **keine** `nid`-Spanne, der Nachbar `#auth: Backend` +schon. 337 Tests, davon 6 neue; der eine alte, der die frühere Regel festhielt, +ist umgeschrieben und benennt jetzt diese. diff --git a/docs/SPEC.md b/docs/SPEC.md index a393017..2376d7d 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -25,7 +25,9 @@ dieser Reihenfolge (wichtig für Kollisionsfreiheit): 6. Knoten-ID: das **erste** alleinstehend angesetzte `#name`-Token (siehe unten). 7. Abhängigkeiten: alle alleinstehend angesetzten `:#a,#b`-Token (siehe unten). 8. Fokusmarke: `!!!` als **alleinstehendes** Token (siehe unten). -9. Rest, whitespace-normalisiert = Label. Leeres Label ⇒ Zeile ignorieren. +9. Rest, whitespace-normalisiert = Label. Leeres Label ⇒ Zeile ignorieren — + **außer** die Zeile trägt eine Knoten-ID; dann wird `#id` das Label + (siehe unten). **Fortsetzungszeile `\`** — eine lange Zeile darf auf mehrere Textzeilen verteilt werden, ohne dass ein neuer Knoten entsteht: @@ -102,8 +104,15 @@ sie ist die Adresse für Abhängigkeiten und Beschreibungsblöcke (§11). **Leerraum oder Zeilenende** folgt; ein Doppelpunkt im Label (`#auth: Regel: nur mit Token`) bleibt also stehen, und `#auth:#db` bleibt ID plus Abhängigkeit. Die Stellung ist frei — `Backend #auth` bedeutet dasselbe. -- Die ID gehört **nicht** zum Label. Eine eigene Darstellung im Diagramm hat - sie (noch) nicht; sichtbar ist sie im Knoten-Tooltip und im `aria-label`. +- Die ID gehört **nicht** zum Label. Sichtbar ist sie im Knoten-Tooltip, im + `aria-label` und — auf Wunsch — vor dem Titel (§9, `#`-Umschalter). +- **Ohne Titel vertritt die ID ihn:** Bleibt nach der Extraktion kein Label + übrig, die Zeile trägt aber eine ID, dann ist `#id` das Label — mit + Doppelpunkt geschrieben (`#US-123:`) wie ohne. Die Zeile wird also **nicht** + ignoriert, sondern ein gewöhnlicher Knoten, und die ID ist vergeben. Gedacht + für den Fall, dass die Kennung schon der Name ist (Ticket-Referenzen, §11) — + den Titel danebenzuschreiben wäre eine Verdopplung. Der `#`-Umschalter (§9) + setzt bei so einem Knoten **nichts** davor: Die ID steht bereits da. - **Doppelte ID:** Warnung `duplicateId` mit beiden Zeilennummern; die spätere ID gilt trotzdem am Knoten (fehlertolerant wie §4 — die Zeile geht nicht verloren). diff --git a/frontend/public/llms.md b/frontend/public/llms.md index b2716b1..88a940b 100644 --- a/frontend/public/llms.md +++ b/frontend/public/llms.md @@ -138,7 +138,12 @@ One node per line. Everything except the label is optional. shared pointer for collaborative editing. Independent of status and necessity; `Wow!!!` stays a label. 8. Whatever remains, whitespace-normalized, is the **label**. An empty label - means the line is ignored. + means the line is ignored — **unless the line carries a node ID**: then + `#id` becomes the label, with or without the trailing colon. `- #US-123` + and `- #US-123:` both render a node titled `#US-123`. Use this when the + identifier already is the name (ticket references); writing the title next + to it would just repeat it. The `#` toggle in the diagram adds no prefix to + such a node — the ID is already there. ### Dependency semantics diff --git a/frontend/src/parser.js b/frontend/src/parser.js index fe9c4b6..f64ae85 100644 --- a/frontend/src/parser.js +++ b/frontend/src/parser.js @@ -288,7 +288,13 @@ export function parse(text){ der führende Leerraum wird mitgefangen und wieder eingesetzt. */ let focus = false; rest = rest.replace(/(^|\s)!!!(?=\s|$)/g, (s, pre) => { focus = true; return pre; }); - const label = rest.replace(/\s+/g, ' ').trim(); + /* Bleibt kein Titel übrig, vertritt die ID ihn (SPEC §1): `- #US-123` + ergibt einen Knoten mit dem Label `#US-123`. Ohne ID bleibt es dabei, + dass eine labellose Zeile keine ist. `labelFromId` merkt den Fall — der + `#`-Umschalter (§9) darf die ID dann nicht ein zweites Mal davorsetzen. */ + let label = rest.replace(/\s+/g, ' ').trim(); + const labelFromId = !label && id != null; + if(labelFromId) label = '#' + id; if(!label) return; let status = null; @@ -308,7 +314,7 @@ export function parse(text){ while(stack.length > 1 && stack[stack.length-1].width >= width) stack.pop(); const parent = stack[stack.length-1].node; - const node = {label, type, optional, fold, status, url, size, tags, id, deps, desc:null, descLines:null, focus, children:[], line:i+1}; + const node = {label, labelFromId, type, optional, fold, status, url, size, tags, id, deps, desc:null, descLines:null, focus, children:[], line:i+1}; parent.children.push(node); stack.push({node, width}); lastNode = node; diff --git a/frontend/src/render.js b/frontend/src/render.js index 5f20995..78728eb 100644 --- a/frontend/src/render.js +++ b/frontend/src/render.js @@ -75,7 +75,9 @@ function nodeAria(n, opts, fold){ if(n.tags && n.tags.length) parts.push(t('a11yTags', {names: n.tags.join(', ')})); /* Knoten-ID und Abhängigkeiten (SPEC §1, D36/D37): keine eigene Darstellung im Diagramm — sichtbar nur im Tooltip und hier. */ - if(n.id) parts.push(t('a11yId', {id: n.id})); + /* Nicht, wenn das Label die ID selbst ist (SPEC §1) — sonst hört ein + Screenreader sie zweimal hintereinander. */ + if(n.id && !n.labelFromId) parts.push(t('a11yId', {id: n.id})); if(n.deps && n.deps.length) parts.push(t('a11yDeps', {ids: n.deps.map(d => '#' + d).join(', ')})); if(n.optional) parts.push(t('a11yOptional')); @@ -126,7 +128,7 @@ function nodeHtml(n, extra, opts, fold){ bleibt schmaler als die Fakten-Zeile (die den Sprung-Hinweis enthält), verbreitert den Tooltip also nicht. Der `aria-label` bekommt ihn NICHT: ein Screenreader läse die Striche einzeln vor (nodeAria oben). */ - const facts = [n.id ? '#' + n.id : '', + const facts = [(n.id && !n.labelFromId) ? '#' + n.id : '', n.deps && n.deps.length ? '→ ' + n.deps.map(d => '#' + d).join(', ') : '', effKey ? t('heldTooltip', {eff: t('st_' + effKey), own: t('st_' + n.status.key)}) @@ -179,7 +181,9 @@ function nodeHtml(n, extra, opts, fold){ Diagramm-Kopf; als Renderer-Option und nicht per CSS versteckt, damit der Grafikexport (er liest den Knotentext) von selbst folgt. `aria-hidden`: Der Screenreader bekommt die ID schon über `a11yId` (D56). */ - const idHtml = showIds && n.id + /* Nicht bei einem Knoten, dessen Label die ID selbst IST (SPEC §1) — sonst + stünde dort `#US-123: #US-123`. */ + const idHtml = showIds && n.id && !n.labelFromId ? ` ` : ''; const inner = foldHtml + diff --git a/frontend/tests/ids.test.js b/frontend/tests/ids.test.js index 012373e..65be2e0 100644 --- a/frontend/tests/ids.test.js +++ b/frontend/tests/ids.test.js @@ -42,11 +42,39 @@ describe('Parser — `#name` als Knoten-ID', () => { expect(nodes.map(n => n.id)).toEqual(['123', 'größe-1']); }); - it('ignoriert eine Zeile, die nur aus einer ID besteht — die ID bleibt frei', () => { - const {roots: r, warnings} = parse(`- #auth\n- [ ] Echt #auth`); - expect(r.map(n => [n.label, n.id])).toEqual([['Echt', 'auth']]); + /* Bis D60 war so eine Zeile keine: Sie wurde ignoriert und die ID blieb + frei. Jetzt vertritt die ID den fehlenden Titel — gedacht für den Fall, + dass die Kennung schon der Name ist (Ticket-Referenzen). */ + it('macht aus einer Zeile mit NUR einer ID einen Knoten mit `#id` als Titel', () => { + const {roots: r, warnings} = parse('- #auth'); + expect(r.map(n => [n.label, n.id, n.labelFromId])).toEqual([['#auth', 'auth', true]]); expect(warnings).toEqual([]); }); + + it('nimmt den Doppelpunkt dabei mit weg', () => { + expect(parse('- #US-123:').roots.map(n => n.label)).toEqual(['#US-123']); + }); + + it('vergibt die ID dabei wirklich — eine zweite meldet sich', () => { + const {roots: r, warnings} = parse('- #auth\n- [ ] Echt #auth'); + expect(r.map(n => n.label)).toEqual(['#auth', 'Echt']); + expect(warnings).toEqual([{type: 'duplicateId', line: 2, id: 'auth', firstLine: 1}]); + }); + + it('setzt `labelFromId` NICHT, wenn ein Titel dasteht', () => { + expect(parse('- #auth: Backend').roots[0].labelFromId).toBe(false); + }); + + /* Die übrigen Bestandteile werden ganz normal gelesen — übrig bleibt nur + kein Titel. */ + it('liest Größe, Status und Tag auch ohne Titel', () => { + const n = parse('- [x] #US-123 (L) @anna').roots[0]; + expect([n.label, n.size, n.tags, n.status.key]).toEqual(['#US-123', 'L', ['anna'], 'fertig']); + }); + + it('lässt eine Zeile ohne ID und ohne Label weiterhin weg', () => { + expect(parse('- (L) @anna').roots).toEqual([]); + }); }); /* Übliche Schreibweise: ID vor dem Titel, abgetrennt durch einen Doppelpunkt.