From 285d86a2e11a43025d11125f5dbd01ca4a2107f5 Mon Sep 17 00:00:00 2001 From: mhoennig Date: Mon, 24 Aug 2026 11:47:16 +0200 Subject: [PATCH] feat(editor): ID-Kurzschreibweise `#.kc` als Eingabehilfe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gefragt war, ob `#.kc` unter `#prod-stage` als Notation `#prod-stage.kc` bedeuten könnte. Als Notation wäre es falsch, und das zeigt der eigene Plan: 181 IDs im Baumteil, davon 50 (28 %), deren gepunktetes Präfix gar nicht die Eltern-ID ist (`#ed.parser` hängt unter `#ed.live`) — die Punkte benennen den BEREICH, nicht den Pfad, genau wie D48 es vorsieht. Dazu: der Beschreibungsteil hinter `---` hat keinen Baum (zwei Schreibweisen für eine ID), Einrücken änderte die Identität und bräche `:#…`-Verweise, und aus einer Konvention würde Grammatik. Gewinn wären 802 Zeichen in 40 kB. Also als Eingabehilfe: getippt wird `#.kc`, beim Verlassen der Zeile steht `#prod-stage.kc` im Text — derselbe Griff, mit dem das Umklappen im Diagramm seine Faltmarke zurückschreibt (D38-Nachtrag 2). Die Datei bleibt eindeutig, durchsuchbar und umsortierbar; llms.md ist unberührt, weil sich an der Notation nichts ändert. - parser.js: `expandShortIds(text)` neben `setFoldMark`, 17 Tests. - app.js: löst beim Zeilenwechsel auf, aber nur in der Zeile, in der auch getippt wurde — ein bloß durchgeklicktes fremdes Dokument bleibt zeichengenau stehen (sonst fiele es aus dem Nachziehen, D27). Falle, zum zweiten Mal dieselbe: `execCommand` verweigert re-entrant im `input`-Ereignis den Dienst, `replaceTextUndoable` fällt dann auf `src.value =` zurück und löscht die Undo-Historie. Gemessen: erstes Rückgängig ohne Wirkung, jedes weitere `false`. Jetzt einen Zug später (`setTimeout 0`) und nur mit Fokus im Textfeld; danach nimmt ein Rückgängig genau die Auflösung zurück. SPEC §9 + D55; Plan: #ed.shortid [x] (159 Knoten, 0 Warnungen). 288 Tests grün (271+17). Co-Authored-By: Claude Opus 5 --- docs/DECISIONS.md | 80 ++++++++++++++++++++++ docs/SPEC.md | 15 ++++ docs/examples/werkbaum.werkbaum | 6 ++ frontend/src/app.js | 41 ++++++++++- frontend/src/parser.js | 53 +++++++++++++++ frontend/tests/shortids.test.js | 117 ++++++++++++++++++++++++++++++++ 6 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 frontend/tests/shortids.test.js diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 5a6943d..a3de911 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -3842,3 +3842,83 @@ als der Fehler längst weg war. Auseinandergehalten hat es der `?t=`-Stempel im Stacktrace gegen den der geladenen Datei (`performance.getEntriesByType`): `…961240` gegen `…049984`. Dieselbe Sorte Lehre wie D25 und D17-Nachtrag 4 — die Meldung des Werkzeugs ist noch kein Befund. + +## D55 — ID-Kurzschreibweise: Eingabehilfe statt Notation +Gefragt war, ob man unter `#prod-stage` einfach `#.kc` schreiben könnte, das +dann `#prod-stage.kc` bedeutet. Die Beobachtung dahinter stimmt: Die +D48-Konvention `#bereich.task` wiederholt das Präfix auf jeder Zeile, und die +Einrückung sagt dasselbe noch einmal. + +**Als Notation wäre es trotzdem falsch — das zeigt der eigene Plan.** Gemessen +an `docs/examples/werkbaum.werkbaum`: 181 IDs im Baumteil, davon **50 (28 %), +deren gepunktetes Präfix gar nicht die Eltern-ID ist** — `#ed.parser` hängt +unter `#ed.live`, `#not.status` unter `#not.line`. Das ist kein Schlendrian, +sondern D48 wörtlich („dritte Stufe nur, wo es sonst kryptisch würde"): Die +Punkte benennen den **Bereich**, nicht den Pfad. Eine Kurzform, die gegen den +Elternknoten auflöst, wäre also für über ein Viertel der IDs des Dokuments +falsch, das die Konvention erfunden hat. Der Gewinn wäre klein: 802 Zeichen in +einer 40-kB-Datei, rund 2 %. + +Drei weitere Kosten, jede für sich schwerer als das Tippen: + +- **Der Beschreibungsteil hat keinen Baum.** Hinter `---` steht der Block-Kopf + uneingerückt und ohne Vorfahren; dort müsste weiterhin die volle ID stehen. + Eine ID, zwei Schreibweisen — je nachdem, wo man sie hinschreibt. +- **Die ID wäre keine Adresse mehr.** Heute überlebt sie das Umsortieren, und + genau darauf bauen die Abhängigkeiten `:#…`, die bewusst quer zum Baum zeigen + (D34). Mit Kurzform ändert **Einrücken** die Identität, und Verweise zeigen + still ins Leere. Dieselbe Zerbrechlichkeit, wegen der D34 den Verweis auf den + *Titel* verworfen hat. +- **Aus einer Konvention würde Grammatik.** Der Punkt ist heute ein gewöhnliches + Zeichen der ID-Zeichenmenge (SPEC §1); dass `#ed.closure` unter `#ed` hängt, + prüft niemand. Machte man ihn strukturell, wären die 50 Fälle oben + Regelverstöße. + +**Entschieden (Nutzer): die Kurzform als Eingabehilfe.** Getippt wird `#.kc`, +und beim **Verlassen der Zeile** steht `#prod-stage.kc` im Text. Der +Präzedenzfall liegt im Haus: Umklappen im Diagramm schreibt seine Faltmarke +ebenso zurück (D38-Nachtrag 2). Damit bleibt die Datei eindeutig, durchsuchbar +und umsortierbar — die Ersparnis beim Tippen gibt es trotzdem, und keine der +drei Kosten oben fällt an. Dass die Auflösung den **Baum**-Vorfahren nimmt und +nicht das Bereichs-Präfix, ist dabei kein Mangel: Man sieht das Ergebnis sofort +im Text und kann es hinschreiben, wie man es will. + +**Aufgelöst wird gegen den nächsten Vorfahren MIT ID**, nicht gegen den +direkten Elternknoten — der kann selbst keine haben. Findet sich keiner +(Wurzelzeile) oder trägt er selbst noch eine Kurzform, bleibt die Zeile +unangetastet: lieber `#.kc` stehen lassen als etwas Falsches hineinschreiben. +Das ist auch deshalb ungefährlich, weil `#.kc` schon heute eine **gültige** ID +ist (der Punkt gehört zur Zeichenmenge) — es geht also nichts verloren, es ist +nur nicht aufgelöst. `#..x` wird nicht angefasst: zwei Punkte haben keine +vereinbarte Bedeutung. + +**Angefasst wird nur die eine Zeile, in der auch getippt wurde.** Beides ist +nötig: Wer ein fremdes Dokument bloß durchklickt, darf es nicht umgeschrieben +bekommen — es fiele sonst aus dem Nachziehen mitgelieferter Fassungen (D27) +und würde als bearbeitet geführt. Nachgemessen: Ein per Dokumentwechsel +geladener Text mit zwei Kurzformen bleibt beim Durchklicken zeichengenau +stehen. + +**Die Falle beim Bauen, zum zweiten Mal dieselbe:** Der Zeilenwechsel kommt oft +aus dem `input`-Ereignis der Enter-Taste, und `execCommand` verweigert den +Dienst, wenn es **re-entrant** darin aufgerufen wird. `replaceTextUndoable` +fällt dann auf `src.value =` zurück — und das löscht die Undo-Historie +(D38-Nachtrag 2, D53). Gemessen: erstes Rückgängig ohne Wirkung, jedes weitere +`false`. Geschrieben wird deshalb einen Zug später (`setTimeout(…, 0)`), und +nur, solange der Fokus im Textfeld steht: Wer die Zeile per Klick ins Diagramm +verlässt, soll nicht zurückgerissen werden (`replaceTextUndoable` fokussiert +selbst). Danach nimmt ein Rückgängig genau die Auflösung zurück +(`#stage.kc` → `#.kc`), und der Stapel lebt weiter. + +**Abhängigkeiten bleiben außen vor.** `:#.kc` wäre eine eigene Entscheidung — +relativ wozu, zum verweisenden Knoten? Das ist selten nützlich und mehrdeutig; +die Regel bleibt vorerst auf die Knoten-ID beschränkt. + +**Getestet ist die Regel, nicht die Verdrahtung** — `expandShortIds()` liegt +als Text→Text-Funktion in `parser.js` neben `setFoldMark` und hat 17 +Zusicherungen (Auflösen über mehrere Ebenen, Vorfahren ohne ID überspringen, +Kommentar/Beschreibungsteil/`"`-Zeilen/Abhängigkeiten unangetastet, nur das +erste `#`-Token, zeichengenaue Erhaltung von Einrückung, Zeichen, Statusbox +und Faltmarke, und dass das Ergebnis denselben Baum ergibt wie die von Hand +ausgeschriebene Fassung). Das Zusammenspiel mit dem Textfeld bleibt +Browser-Sache — die Lehre aus D54-Nachtrag 3. diff --git a/docs/SPEC.md b/docs/SPEC.md index 0a389db..b900403 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -703,6 +703,21 @@ verknüpft (siehe D25): - Die Hervorhebung ist eine reine Editierhilfe: nicht im Grafikexport, nicht im Druck. +### Kurzschreibweise der Knoten-ID beim Tippen (`#.name`) +Eine **Eingabehilfe**, keine Notation: Wer `#.kc` unter einem Knoten mit der ID +`#prod-stage` schreibt, findet beim **Verlassen der Zeile** `#prod-stage.kc` im +Text. Aufgelöst wird gegen den nächsten Vorfahren **mit** ID; gibt es keinen +oder trägt er selbst noch eine Kurzform, bleibt die Zeile stehen (`#.kc` ist +ohnehin eine gültige ID — der Punkt gehört zur Zeichenmenge, §1). `#..x` wird +nicht angefasst. + +Die Datei enthält danach immer die volle ID: Sie bleibt durchsuchbar und +überlebt das Umsortieren, worauf die Abhängigkeiten `:#…` bauen. **An der +Notation ändert sich dadurch nichts** — der Parser sieht nie eine Kurzform, +und `llms.md` (§13) bleibt unberührt. Angefasst wird nur die Zeile, in der auch +getippt wurde; Abhängigkeiten (`:#…`) und der Beschreibungsteil hinter `---` +bleiben außen vor. Siehe D55. + ### Zeilennummern im Texteditor Links neben dem Textfeld steht ein schmaler Streifen mit den **Zeilennummern** — dieselben, die die Warnungen nennen („Zeile 12: …", §4). Ohne ihn muss man sie diff --git a/docs/examples/werkbaum.werkbaum b/docs/examples/werkbaum.werkbaum index f394e81..cdfa152 100644 --- a/docs/examples/werkbaum.werkbaum +++ b/docs/examples/werkbaum.werkbaum @@ -67,6 +67,7 @@ - [^] #ed.docs.restore: Restore a shipped document from the menu (XS) - [^] #ed.jump: Jump between diagram and text (S) - [^] #ed.lineno: Line numbers in the text editor (XS) %% the warnings name them + - [x] #ed.shortid: Type #.kc under #prod-stage, get #prod-stage.kc (XS) %% an input aid, not notation - [^] #ed.lineno.tip: The warning is the tooltip of its number (XS) %% no hunting in the list - [^] #ed.fold: Collapsible subtrees (M) - [^] #ed.fold.marks: Initial state from the text: > collapses, < re-opens (S) @@ -492,6 +493,11 @@ word for word as it stands below the diagram. Otherwise you read the number, then hunt for it in the list. +#ed.shortid + The dotted prefix repeats what the indentation already says. Resolved while + typing, not while parsing: the file keeps the full id, so it stays greppable + and survives re-ordering, which is what the :#... references rely on. + #ed.fold A large plan is unreadable unless parts of it can be put away. Folding is view only — warnings from folded subtrees are still reported, because they diff --git a/frontend/src/app.js b/frontend/src/app.js index 68e63d9..674f5e4 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -1,5 +1,5 @@ import './style.css'; -import { parse, setFoldMark } from './parser.js'; +import { parse, setFoldMark, expandShortIds } from './parser.js'; import { computeCheapPlan, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, atMostM } from './model.js'; import { esc, renderTreeHtml, TIP_RULE } from './render.js'; import { formatWarning, warningText } from './warnings.js'; @@ -1348,11 +1348,48 @@ function caretLineOf(){ function syncCaret(){ const line = caretLineOf(); const moved = line !== caretLine; - caretLine = line; + if(moved) resolveShortId(caretLine); + caretLine = caretLineOf(); /* neu lesen: das Auflösen kann den Text ändern */ highlightCurrentNode(moved); } for(const ev of ['click','keyup','input','focus']) src.addEventListener(ev, syncCaret); +/* Kurzschreibweise der ID auflösen (D55): `#.kc` wird beim **Verlassen der + Zeile** zu `#prod-stage.kc`. Eingabehilfe, keine Notation — in der Datei + steht danach die volle ID, sie bleibt also durchsuchbar und überlebt das + Umsortieren. + + Angefasst wird nur die **eine** Zeile, in der auch getippt wurde. Beides ist + nötig: `#.foo` ist schon heute eine gültige ID, und wer ein fremdes Dokument + bloß durchklickt, darf es nicht umgeschrieben bekommen (und damit aus dem + Nachziehen mitgelieferter Fassungen fallen, D27). Geschrieben wird + undo-fähig — ein Griff daneben kostet ein Strg+Z (D53). */ +let touchedLine = null; +src.addEventListener('input', () => { touchedLine = caretLineOf(); }); +function resolveShortId(line){ + if(src.readOnly || line == null || line !== touchedLine) return; + touchedLine = null; + /* **Nicht** sofort schreiben: Der Zeilenwechsel kommt oft aus dem + `input`-Ereignis der Enter-Taste, und `execCommand` verweigert den Dienst, + wenn es re-entrant darin aufgerufen wird. `replaceTextUndoable` fiele dann + auf `src.value =` zurück — und das löscht die Undo-Historie (D38-Nachtrag + 2). Gemessen: erstes Rückgängig ohne Wirkung, jedes weitere `false`. + Deshalb ein Zug später, wenn das Ereignis zugestellt ist. + + Nur mit Fokus im Textfeld: Wer die Zeile per Klick ins Diagramm verlässt, + soll nicht zurückgerissen werden (`replaceTextUndoable` fokussiert selbst). + Die Kurzform bleibt dann stehen — sie ist eine gültige ID, es geht nichts + verloren, und beim nächsten Bearbeiten der Zeile wird sie aufgelöst. */ + setTimeout(() => { + if(document.activeElement !== src) return; + const lines = src.value.split('\n'); + const neu = expandShortIds(src.value).split('\n'); + if(line > lines.length || neu[line-1] === lines[line-1]) return; + lines[line-1] = neu[line-1]; + replaceTextUndoable(lines.join('\n')); + }, 0); +} + /* Gegenstück zum Alt+Klick am Knoten (D25, Nachtrag): Alt+Klick im Textfeld — Tastatur Alt+Enter — holt den Knoten der Cursor-Zeile in die **Mitte** des Diagramms und gibt ihm den Fokus. Der gewöhnliche Klick markiert ihn zwar diff --git a/frontend/src/parser.js b/frontend/src/parser.js index a7b0788..bf41634 100644 --- a/frontend/src/parser.js +++ b/frontend/src/parser.js @@ -36,6 +36,59 @@ export function setFoldMark(line, mark){ return m[1] + m[2] + (mark ? mark + ' ' : '') + line.slice(m[0].length); } +const RE_LINE = /^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:([><])(?=[ \t])\s*)?(?:\[([^\]])\]\s*)?(?:([><])(?=[ \t])\s*)?(.*)$/; +const RE_ID_TOKEN = /(^|\s)#([\p{L}\p{N}._-]+)/u; + +/* Kurzschreibweise der Knoten-ID auflösen: `#.kc` unter `#prod-stage` wird zu + `#prod-stage.kc` (D55). Das ist eine **Eingabehilfe**, keine Notation — die + Datei enthält am Ende immer die volle ID. Deshalb steht sie hier als + Text→Text-Funktion neben `setFoldMark`: Der Editor ruft sie beim Verlassen + der Zeile auf und schreibt das Ergebnis zurück, wie das Umklappen im + Diagramm seine Faltmarke zurückschreibt (D38-Nachtrag 2). + + Aufgelöst wird gegen den **nächsten Vorfahren mit ID** — nicht zwingend den + direkten Elternknoten, der kann selbst ohne ID sein. Findet sich keiner + (Wurzelzeile) oder trägt er selbst noch eine Kurzform, bleibt die Zeile + unangetastet: Lieber `#.kc` stehen lassen, als etwas Falsches hineinschreiben. + Der Beschreibungsteil hinter `---` hat keinen Baum und wird nicht angefasst. */ +export function expandShortIds(text){ + const lines = text.split('\n'); + const stack = []; /* {width, id} — auch Knoten OHNE ID stehen drin */ + let changed = false; + for(let i = 0; i < lines.length; i++){ + const raw = lines[i]; + if(/^\s*-{3,}\s*$/.test(raw)) break; + const k = raw.indexOf('%%'); + const head = k === -1 ? raw : raw.slice(0, k); + const tail = k === -1 ? '' : raw.slice(k); + const m = head.match(RE_LINE); + if(!m) continue; + const body = m[6]; + if(!body.trim()) continue; /* leer oder nur Kommentar */ + if(/^"(\s|$)/.test(body)) continue; /* Beschreibungszeile, kein Knoten */ + const width = m[1].replace(/\t/g, ' ').length; + while(stack.length && stack[stack.length - 1].width >= width) stack.pop(); + const t = body.match(RE_ID_TOKEN); + let id = t ? t[2] : null; + /* `.kc` ja, `..kc` nein — zwei Punkte sind keine vereinbarte Bedeutung. */ + if(id && /^\.[^.]/.test(id)){ + let anc = null; + for(let j = stack.length - 1; j >= 0; j--){ + if(stack[j].id && stack[j].id[0] !== '.'){ anc = stack[j].id; break; } + } + if(anc){ + id = anc + id; + lines[i] = head.slice(0, head.length - body.length) + + body.replace(RE_ID_TOKEN, (s, pre) => pre + '#' + id) + + tail; + changed = true; + } + } + stack.push({width, id}); + } + return changed ? lines.join('\n') : text; +} + /* Status, die als „realisiert" zählen (XOR-Regel, SPEC §3/D35): Kosten sind investiert oder mehr. Absicht (`[?]`, `[ ]`, `[!]`), Ablehnung (`[-]`) und neutrale Knoten zählen nicht. */ diff --git a/frontend/tests/shortids.test.js b/frontend/tests/shortids.test.js new file mode 100644 index 0000000..7c88a18 --- /dev/null +++ b/frontend/tests/shortids.test.js @@ -0,0 +1,117 @@ +import { describe, it, expect } from 'vitest'; +import { expandShortIds, parse } from '../src/parser.js'; + +/* Kurzschreibweise der Knoten-ID (D55): `#.kc` unter `#prod-stage` wird beim + Verlassen der Zeile zu `#prod-stage.kc`. Eingabehilfe, keine Notation — in + der Datei steht danach immer die volle ID. */ + +describe('expandShortIds — auflösen gegen den nächsten Vorfahren mit ID', () => { + it('löst gegen den Elternknoten auf', () => { + const text = ['#prod-stage: Stage', ' - #.kc: Keycloak'].join('\n'); + expect(expandShortIds(text)).toBe( + ['#prod-stage: Stage', ' - #prod-stage.kc: Keycloak'].join('\n')); + }); + + it('löst über mehrere Ebenen jeweils gegen die nächste auf', () => { + const text = ['#a: A', ' - #.b: B', ' - #.c: C'].join('\n'); + /* Die zweite Zeile ist beim Auflösen der dritten bereits `#a.b`. */ + expect(expandShortIds(text)).toBe( + ['#a: A', ' - #a.b: B', ' - #a.b.c: C'].join('\n')); + }); + + /* Der direkte Elternknoten muss keine ID haben — dann zählt der nächste + Vorfahre, der eine hat. */ + it('überspringt Vorfahren ohne ID', () => { + const text = ['#a: A', ' - Zwischenknoten ohne ID', ' - #.c: C'].join('\n'); + expect(expandShortIds(text)).toContain('#a.c: C'); + }); + + it('nimmt mehrere Stufen auf einmal', () => { + const text = ['#a: A', ' - #.b.c: tief'].join('\n'); + expect(expandShortIds(text)).toContain('#a.b.c: tief'); + }); + + it('lässt volle IDs unangetastet', () => { + const text = ['#a: A', ' - #ganz.anders: X'].join('\n'); + expect(expandShortIds(text)).toBe(text); + }); + + it('gibt denselben String zurück, wenn nichts aufzulösen ist', () => { + const text = '- [x] Nichts zu tun (M)'; + expect(expandShortIds(text)).toBe(text); + }); +}); + +describe('expandShortIds — wo nicht aufgelöst werden kann, bleibt der Text stehen', () => { + it('lässt eine Wurzelzeile in Ruhe (kein Vorfahre)', () => { + const text = '#.kc: Keycloak'; + expect(expandShortIds(text)).toBe(text); + }); + + it('löst nicht gegen einen Vorfahren auf, der selbst noch kurz ist', () => { + const text = ['#.a: A', ' - #.b: B'].join('\n'); + expect(expandShortIds(text)).toBe(text); + }); + + /* Zwei Punkte sind keine vereinbarte Bedeutung — also nichts erfinden. */ + it('rührt `#..x` nicht an', () => { + const text = ['#a: A', ' - #..x: X'].join('\n'); + expect(expandShortIds(text)).toBe(text); + }); +}); + +describe('expandShortIds — fasst nur an, was die ID ist', () => { + it('lässt den Beschreibungsteil hinter --- unberührt', () => { + const text = ['#a: A', ' - #.b: B', '---', '#.b', ' Text mit #.b darin'].join('\n'); + const out = expandShortIds(text).split('\n'); + expect(out[1]).toContain('#a.b: B'); + expect(out[3]).toBe('#.b'); + expect(out[4]).toBe(' Text mit #.b darin'); + }); + + it('lässt eine Kurzform im Kommentar stehen', () => { + const text = ['#a: A', ' - #.b: B %% später vielleicht #.c'].join('\n'); + expect(expandShortIds(text)).toBe( + ['#a: A', ' - #a.b: B %% später vielleicht #.c'].join('\n')); + }); + + it('fasst nur das ERSTE #-Token an (SPEC §1)', () => { + const text = ['#a: A', ' - #.b: siehe auch #.c'].join('\n'); + expect(expandShortIds(text)).toContain('#a.b: siehe auch #.c'); + }); + + it('lässt eine `"`-Beschreibungszeile aus', () => { + const text = ['#a: A', ' " Zitat mit #.b', ' - #.c: C'].join('\n'); + const out = expandShortIds(text).split('\n'); + expect(out[1]).toBe(' " Zitat mit #.b'); + expect(out[2]).toContain('#a.c: C'); + }); + + it('erhält Einrückung, Zeichen, Statusbox und Faltmarke zeichengenau', () => { + const text = ['#a: A', ' = [x] > #.b: B (M) @anna :#a !!!'].join('\n'); + expect(expandShortIds(text)).toBe( + ['#a: A', ' = [x] > #a.b: B (M) @anna :#a !!!'].join('\n')); + }); + + /* Die Abhängigkeits-Schreibweise beginnt mit `:#` und ist damit kein + alleinstehendes `#`-Token — sie bleibt außen vor (Kurzform dort wäre eine + eigene Entscheidung, D55). */ + it('rührt Abhängigkeiten nicht an', () => { + const text = ['#a: A', ' - Kind :#.b'].join('\n'); + expect(expandShortIds(text)).toBe(text); + }); +}); + +describe('expandShortIds — das Ergebnis parst wie eine von Hand geschriebene ID', () => { + it('ergibt denselben Baum wie die ausgeschriebene Fassung', () => { + const kurz = ['#prod-stage: Stage', ' - #.kc: Keycloak'].join('\n'); + const lang = ['#prod-stage: Stage', ' - #prod-stage.kc: Keycloak'].join('\n'); + expect(parse(expandShortIds(kurz))).toEqual(parse(lang)); + }); + + it('macht aus der Kurzform eine auflösbare Abhängigkeit', () => { + const text = ['#a: A', ' - #.b: B', ' - #.c: C :#a.b'].join('\n'); + const r = parse(expandShortIds(text)); + expect(r.warnings).toEqual([]); /* kein unknownDep mehr */ + }); +});