feat: Größe ist das letzte alleinstehende (L)-Token der Zeile (D68)
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
9999c7973e
commit
59da485af6
@@ -19,6 +19,7 @@ reverse.
|
|||||||
|
|
||||||
## 2026-08-25
|
## 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
|
- 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
|
- Ctrl+click a dependency `:#id` in the text jumps to the line that defines the ID — Ctrl+Enter does the same at the caret
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
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
|
einem korrekt gebauten `KeyboardEvent`; der Klick-Weg ließ sich dagegen echt
|
||||||
auslösen.
|
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.
|
||||||
|
|||||||
+25
-1
@@ -20,7 +20,9 @@ dieser Reihenfolge (wichtig für Kollisionsfreiheit):
|
|||||||
(`>` / `<`) per Zeilen-Regex; `=` nur mit folgendem Leerraum (§3),
|
(`>` / `<`) per Zeilen-Regex; `=` nur mit folgendem Leerraum (§3),
|
||||||
die Faltmarke ebenso (siehe unten).
|
die Faltmarke ebenso (siehe unten).
|
||||||
3. URL: erstes Token, das auf `https?://\S+` passt (dadurch stören `@` in URLs nicht).
|
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.
|
5. Tags: alle `@name`-Vorkommen.
|
||||||
6. Knoten-ID: das **erste** alleinstehend angesetzte `#name`-Token (siehe unten).
|
6. Knoten-ID: das **erste** alleinstehend angesetzte `#name`-Token (siehe unten).
|
||||||
7. Abhängigkeiten: alle alleinstehend angesetzten `:#a,#b`-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
|
Bild sagen dasselbe. Sie sagen nichts über Fortschritt (§4) oder
|
||||||
Notwendigkeit (§3) und ändern weder Kosten noch Warnungen.
|
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;
|
**Knoten-ID `#name`** — benennt einen Knoten im **ganzen Dokument** eindeutig;
|
||||||
sie ist die Adresse für Abhängigkeiten und Beschreibungsblöcke (§11).
|
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]*)?
|
^([ \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
|
Für die Knoten-ID (Schritt 6, nur der erste Treffer; die letzte Gruppe ist der
|
||||||
optionale Trenn-Doppelpunkt, der mit entfällt):
|
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)
|
## 5. Aufwand (T-Shirt-Größen)
|
||||||
|
|
||||||
- Werte: `XS < S < M < L < XL < XXL`, notiert in Klammern, z. B. `(L)`.
|
- 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.
|
- **Untergliederungsregel:** Ab `(M)` muss ein Element weiter zerlegt sein.
|
||||||
Ein Element ≥ M **ohne Kinder** erhält einen Geister-Knoten an gestrichelter
|
Ein Element ≥ M **ohne Kinder** erhält einen Geister-Knoten an gestrichelter
|
||||||
Linie darunter (in `--warn`, `#B45309`). Sein Label ist knapp „…“; die
|
Linie darunter (in `--warn`, `#B45309`). Sein Label ist knapp „…“; die
|
||||||
|
|||||||
@@ -105,7 +105,10 @@ One node per line. Everything except the label is optional.
|
|||||||
|
|
||||||
### Size (effort)
|
### 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
|
- From `(M)` upward a node **should be decomposed further**; a node ≥ M
|
||||||
without children gets a placeholder hint in the diagram.
|
without children gets a placeholder hint in the diagram.
|
||||||
- For cost estimation a missing size is **estimated from the sub-packages**:
|
- 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.
|
(whole line or trailing part). This also applies inside descriptions.
|
||||||
2. Bare `https?://…` URL — makes the node clickable; extracted early so `@`,
|
2. Bare `https?://…` URL — makes the node clickable; extracted early so `@`,
|
||||||
`#` and `!!!` inside URLs never trigger.
|
`#` 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:
|
4. `@name` — people tags; several per line, any position. Characters:
|
||||||
Unicode letters, digits, `.`, `_`, `-`.
|
Unicode letters, digits, `.`, `_`, `-`.
|
||||||
5. `#id` — **node ID**: the *first* free-standing `#token` of the line
|
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
|
- Keep rejected alternatives as `[-]` instead of deleting them: the decision
|
||||||
stays visible (add the reason as a `%%` comment).
|
stays visible (add the reason as a `%%` comment).
|
||||||
- To mention syntax literally in a label without triggering it, wrap it in
|
- 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
|
## Complete example
|
||||||
|
|
||||||
|
|||||||
+15
-1
@@ -286,7 +286,21 @@ export function parse(text){
|
|||||||
let rest = m[6], url = null, size = null;
|
let rest = m[6], url = null, size = null;
|
||||||
const tags = [];
|
const tags = [];
|
||||||
rest = rest.replace(/https?:\/\/\S+/i, s => { url = s; return ''; });
|
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 ''; });
|
rest = rest.replace(/@([\p{L}\p{N}._-]+)/gu, (s, g) => { tags.push(g); return ''; });
|
||||||
/* Knoten-ID `#name` (SPEC §1, D36): nur ALLEINSTEHEND ANGESETZT — „C#"
|
/* Knoten-ID `#name` (SPEC §1, D36): nur ALLEINSTEHEND ANGESETZT — „C#"
|
||||||
bleibt Label, und das reservierte `:#a,#b` (§11) wird nicht gefressen.
|
bleibt Label, und das reservierte `:#a,#b` (§11) wird nicht gefressen.
|
||||||
|
|||||||
@@ -128,6 +128,35 @@ describe('parse — Randfälle', () => {
|
|||||||
expect(roots[1].size).toBe('M');
|
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', () => {
|
it('ohne Angaben: status/url/size null, tags leer', () => {
|
||||||
const { roots } = parse('- Nackt');
|
const { roots } = parse('- Nackt');
|
||||||
expect(roots[0]).toMatchObject({ status: null, url: null, size: null, tags: [] });
|
expect(roots[0]).toMatchObject({ status: null, url: null, size: null, tags: [] });
|
||||||
|
|||||||
Reference in New Issue
Block a user