feat(editor): ID-Kurzschreibweise #.kc als Eingabehilfe

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 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-24 11:47:16 +02:00
co-authored by Claude Opus 5
parent e937e1bbbb
commit 285d86a2e1
6 changed files with 310 additions and 2 deletions
+80
View File
@@ -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`): im Stacktrace gegen den der geladenen Datei (`performance.getEntriesByType`):
`…961240` gegen `…049984`. Dieselbe Sorte Lehre wie D25 und D17-Nachtrag 4 — `…961240` gegen `…049984`. Dieselbe Sorte Lehre wie D25 und D17-Nachtrag 4 —
die Meldung des Werkzeugs ist noch kein Befund. 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.
+15
View File
@@ -703,6 +703,21 @@ verknüpft (siehe D25):
- Die Hervorhebung ist eine reine Editierhilfe: nicht im Grafikexport, nicht im - Die Hervorhebung ist eine reine Editierhilfe: nicht im Grafikexport, nicht im
Druck. 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 ### Zeilennummern im Texteditor
Links neben dem Textfeld steht ein schmaler Streifen mit den **Zeilennummern** Links neben dem Textfeld steht ein schmaler Streifen mit den **Zeilennummern**
dieselben, die die Warnungen nennen („Zeile 12: …", §4). Ohne ihn muss man sie dieselben, die die Warnungen nennen („Zeile 12: …", §4). Ohne ihn muss man sie
+6
View File
@@ -67,6 +67,7 @@
- [^] #ed.docs.restore: Restore a shipped document from the menu (XS) - [^] #ed.docs.restore: Restore a shipped document from the menu (XS)
- [^] #ed.jump: Jump between diagram and text (S) - [^] #ed.jump: Jump between diagram and text (S)
- [^] #ed.lineno: Line numbers in the text editor (XS) %% the warnings name them - [^] #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.lineno.tip: The warning is the tooltip of its number (XS) %% no hunting in the list
- [^] #ed.fold: Collapsible subtrees (M) - [^] #ed.fold: Collapsible subtrees (M)
- [^] #ed.fold.marks: Initial state from the text: > collapses, < re-opens (S) - [^] #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, word for word as it stands below the diagram. Otherwise you read the number,
then hunt for it in the list. 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 #ed.fold
A large plan is unreadable unless parts of it can be put away. Folding is 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 view only — warnings from folded subtrees are still reported, because they
+39 -2
View File
@@ -1,5 +1,5 @@
import './style.css'; 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 { computeCheapPlan, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, atMostM } from './model.js';
import { esc, renderTreeHtml, TIP_RULE } from './render.js'; import { esc, renderTreeHtml, TIP_RULE } from './render.js';
import { formatWarning, warningText } from './warnings.js'; import { formatWarning, warningText } from './warnings.js';
@@ -1348,11 +1348,48 @@ function caretLineOf(){
function syncCaret(){ function syncCaret(){
const line = caretLineOf(); const line = caretLineOf();
const moved = line !== caretLine; const moved = line !== caretLine;
caretLine = line; if(moved) resolveShortId(caretLine);
caretLine = caretLineOf(); /* neu lesen: das Auflösen kann den Text ändern */
highlightCurrentNode(moved); highlightCurrentNode(moved);
} }
for(const ev of ['click','keyup','input','focus']) src.addEventListener(ev, syncCaret); 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 /* 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 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 Diagramms und gibt ihm den Fokus. Der gewöhnliche Klick markiert ihn zwar
+53
View File
@@ -36,6 +36,59 @@ export function setFoldMark(line, mark){
return m[1] + m[2] + (mark ? mark + ' ' : '') + line.slice(m[0].length); 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
TextText-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 /* Status, die als realisiert" zählen (XOR-Regel, SPEC §3/D35): Kosten sind
investiert oder mehr. Absicht (`[?]`, `[ ]`, `[!]`), Ablehnung (`[-]`) und investiert oder mehr. Absicht (`[?]`, `[ ]`, `[!]`), Ablehnung (`[-]`) und
neutrale Knoten zählen nicht. */ neutrale Knoten zählen nicht. */
+117
View File
@@ -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 */
});
});