@diagrammo/dgmo 0.8.2 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,10 +4,10 @@
4
4
  //
5
5
  // Parses `infra [Title]` syntax into a structured InfraModel.
6
6
  // Handles: chart metadata, component blocks with indented properties
7
- // and connections, [Group] / # Group containers, tag groups, pipe metadata.
7
+ // and connections, [Group] containers, tag groups, pipe metadata.
8
8
 
9
9
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
10
- import { measureIndent, normalizeDirection, parseFirstLine, GROUP_HASH_RE, OPTION_NOCOLON_RE } from '../utils/parsing';
10
+ import { measureIndent, parseFirstLine, OPTION_NOCOLON_RE } from '../utils/parsing';
11
11
  import { matchTagBlockHeading } from '../utils/tag-groups';
12
12
  import type {
13
13
  ParsedInfra,
@@ -40,12 +40,6 @@ const ASYNC_SIMPLE_CONNECTION_RE =
40
40
  // Deprecated xN fanout suffix (e.g. "x5" at end of line)
41
41
  const DEPRECATED_FANOUT_RE = /\bx(\d+)\s*$/;
42
42
 
43
- // "is a" type declaration: NodeName is a <type>
44
- const IS_A_RE = /^(.+?)\s+is\s+an?\s+(database|cache|queue|service|gateway|storage|function|network)\s*$/i;
45
-
46
- // Valid node types for "is a" declarations
47
- const VALID_NODE_TYPES = new Set(['database', 'cache', 'queue', 'service', 'gateway', 'storage', 'function', 'network']);
48
-
49
43
  // Group declaration: [Group Name] with optional pipe metadata
50
44
  const GROUP_RE = /^\[([^\]]+)\]\s*(?:\|\s*(.+))?$/;
51
45
 
@@ -217,16 +211,9 @@ export function parseInfra(content: string): ParsedInfra {
217
211
  continue;
218
212
  }
219
213
 
220
- // direction LR | TB (also accepts orientation as alias)
221
- // Supports both `direction LR` (new) and `direction: LR` (legacy)
222
- if (/^(?:direction|orientation)\s/i.test(trimmed)) {
223
- const raw = trimmed.replace(/^(?:direction|orientation)\s+/i, '').trim();
224
- const dir = normalizeDirection(raw);
225
- if (dir) {
226
- result.direction = dir;
227
- } else {
228
- warn(lineNumber, `Unknown direction '${raw}'. Expected 'LR', 'TB', 'horizontal', or 'vertical'.`);
229
- }
214
+ // direction-tb bare boolean to switch to top-to-bottom (default is LR)
215
+ if (/^direction-tb$/i.test(trimmed)) {
216
+ result.direction = 'TB';
230
217
  continue;
231
218
  }
232
219
 
@@ -247,23 +234,6 @@ export function parseInfra(content: string): ParsedInfra {
247
234
  continue;
248
235
  }
249
236
 
250
- // scenario: Name — no longer supported
251
- if (/^scenario\s*:/i.test(trimmed)) {
252
- setError(lineNumber, `'scenario:' syntax is no longer supported`);
253
- // Skip indented block
254
- let si = i + 1;
255
- while (si < lines.length) {
256
- const sLine = lines[si];
257
- const sTrimmed = sLine.trim();
258
- if (!sTrimmed || sTrimmed.startsWith('#')) { si++; continue; }
259
- const sIndent = sLine.length - sLine.trimStart().length;
260
- if (sIndent === 0) break;
261
- si++;
262
- }
263
- i = si - 1;
264
- continue;
265
- }
266
-
267
237
  // Tag group: `tag Name [alias]` (via shared matchTagBlockHeading)
268
238
  const tagMatch = matchTagBlockHeading(trimmed);
269
239
  if (tagMatch) {
@@ -278,23 +248,6 @@ export function parseInfra(content: string): ParsedInfra {
278
248
  continue;
279
249
  }
280
250
 
281
- // # GroupName (alternate group notation)
282
- const hashGroupMatch = trimmed.match(GROUP_HASH_RE);
283
- if (hashGroupMatch) {
284
- finishCurrentNode();
285
- finishCurrentTagGroup();
286
- const gLabel = hashGroupMatch[1].trim();
287
- const gId = groupId(gLabel);
288
- currentGroup = {
289
- id: gId,
290
- label: gLabel,
291
- metadata: undefined,
292
- lineNumber,
293
- };
294
- result.groups.push(currentGroup);
295
- continue;
296
- }
297
-
298
251
  // [Group Name] or [Group Name] | t: Engineering
299
252
  const groupMatch = trimmed.match(GROUP_RE);
300
253
  if (groupMatch) {
@@ -313,32 +266,6 @@ export function parseInfra(content: string): ParsedInfra {
313
266
  continue;
314
267
  }
315
268
 
316
- // "is a" type declaration: NodeName is a <type>
317
- const isaMatch = trimmed.match(IS_A_RE);
318
- if (isaMatch) {
319
- finishCurrentNode();
320
- finishCurrentTagGroup();
321
-
322
- const name = isaMatch[1].trim();
323
- const nType = isaMatch[2].toLowerCase();
324
- const id = nodeId(name);
325
- const isEdge = EDGE_NODE_NAMES.has(id.toLowerCase());
326
-
327
- currentNode = {
328
- id,
329
- label: name,
330
- properties: [],
331
- groupId: null,
332
- tags: {},
333
- isEdge,
334
- nodeType: nType,
335
- lineNumber,
336
- };
337
- currentGroup = null;
338
- baseIndent = 0;
339
- continue;
340
- }
341
-
342
269
  // Component at top level (no indent)
343
270
  const compMatch = trimmed.match(COMPONENT_RE);
344
271
  if (compMatch) {
@@ -408,30 +335,6 @@ export function parseInfra(content: string): ParsedInfra {
408
335
  }
409
336
  }
410
337
 
411
- // "is a" type declaration inside group
412
- const isaMatchG = trimmed.match(IS_A_RE);
413
- if (isaMatchG) {
414
- finishCurrentTagGroup();
415
- const name = isaMatchG[1].trim();
416
- const nType = isaMatchG[2].toLowerCase();
417
- const id = nodeId(name);
418
- // Cascade group metadata into node tags (node-level overrides later)
419
- const tags: Record<string, string> = currentGroup.metadata ? { ...currentGroup.metadata } : {};
420
-
421
- currentNode = {
422
- id,
423
- label: name,
424
- properties: [],
425
- groupId: currentGroup.id,
426
- tags,
427
- isEdge: false,
428
- nodeType: nType,
429
- lineNumber,
430
- };
431
- baseIndent = indent;
432
- continue;
433
- }
434
-
435
338
  const compMatch = trimmed.match(COMPONENT_RE);
436
339
  if (compMatch) {
437
340
  finishCurrentTagGroup();
@@ -639,28 +542,6 @@ export function parseInfra(content: string): ParsedInfra {
639
542
  if (currentGroup && indent > 0) {
640
543
  finishCurrentNode();
641
544
 
642
- // "is a" type declaration inside group
643
- const isaMatchG2 = trimmed.match(IS_A_RE);
644
- if (isaMatchG2) {
645
- const name = isaMatchG2[1].trim();
646
- const nType = isaMatchG2[2].toLowerCase();
647
- const id = nodeId(name);
648
- const tags: Record<string, string> = currentGroup.metadata ? { ...currentGroup.metadata } : {};
649
-
650
- currentNode = {
651
- id,
652
- label: name,
653
- properties: [],
654
- groupId: currentGroup.id,
655
- tags,
656
- isEdge: false,
657
- nodeType: nType,
658
- lineNumber,
659
- };
660
- baseIndent = indent;
661
- continue;
662
- }
663
-
664
545
  const compMatch = trimmed.match(COMPONENT_RE);
665
546
  if (compMatch) {
666
547
  const name = compMatch[1];
@@ -780,7 +661,7 @@ export function extractSymbols(docText: string): DiagramSymbols {
780
661
  // Recognize new-style bare options (`key value`) and old-style (`key: value`)
781
662
  const firstLine = parseFirstLine(line);
782
663
  if (firstLine) continue; // chart type line
783
- if (/^(?:direction|orientation|animate|no-animate|slo-|default-)/i.test(line)) continue;
664
+ if (/^(?:direction-tb|animate|no-animate|slo-|default-)/i.test(line)) continue;
784
665
  if (/^[a-z-]+\s*:/i.test(line)) continue; // legacy colon options
785
666
  inMetadata = false;
786
667
  } else {
@@ -794,7 +675,6 @@ export function extractSymbols(docText: string): DiagramSymbols {
794
675
  if (/^tag\s*:/i.test(line)) { inTagGroup = true; continue; } // legacy
795
676
  inTagGroup = false;
796
677
  if (/^\[/.test(line)) continue; // [Group] header
797
- if (/^#\s/.test(line)) continue; // # Group header
798
678
  const m = COMPONENT_RE.exec(line);
799
679
  if (m && !entities.includes(m[1]!)) entities.push(m[1]!);
800
680
  } else {
@@ -62,7 +62,6 @@ export interface InfraNode {
62
62
  groupId: string | null;
63
63
  tags: Record<string, string>; // tagGroup -> tagValue
64
64
  isEdge: boolean; // true for the `edge` entry-point component
65
- nodeType?: string; // database, cache, queue, service, gateway, storage, function, network
66
65
  description?: string;
67
66
  lineNumber: number;
68
67
  }
@@ -203,16 +203,16 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
203
203
  continue;
204
204
  }
205
205
 
206
- // hide directive (no colon): `hide phase Planning, phase Review`
206
+ // hide directive (colon syntax): `hide phase:Planning, phase:Review`
207
207
  const hideMatch = trimmed.match(/^hide\s+(.+)/i);
208
208
  if (hideMatch && !trimmed.match(/^hide\s*\|/)) {
209
- // Parse comma-separated tag-value pairs: `phase Planning, phase Review`
209
+ // Parse comma-separated tag:value pairs: `phase:Planning, phase:Review`
210
210
  const pairs = hideMatch[1].split(',');
211
211
  for (const pair of pairs) {
212
- const tokens = pair.trim().split(/\s+/);
213
- if (tokens.length >= 2) {
214
- const groupKey = tokens[0].toLowerCase();
215
- const value = tokens.slice(1).join(' ').toLowerCase();
212
+ const colonIdx = pair.indexOf(':');
213
+ if (colonIdx > 0) {
214
+ const groupKey = pair.substring(0, colonIdx).trim().toLowerCase();
215
+ const value = pair.substring(colonIdx + 1).trim().toLowerCase();
216
216
  if (groupKey && value) {
217
217
  if (!result.initialHiddenTagValues.has(groupKey)) {
218
218
  result.initialHiddenTagValues.set(groupKey, new Set());
@@ -231,7 +231,7 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
231
231
  const key = optMatch[1].toLowerCase();
232
232
  const value = optMatch[2].trim();
233
233
  // Only recognize known option keys (not node content)
234
- if (key === 'active-tag' || key === 'sort') {
234
+ if (key === 'active-tag') {
235
235
  result.options[key] = value;
236
236
  continue;
237
237
  }
@@ -247,12 +247,6 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
247
247
  );
248
248
  continue;
249
249
  }
250
- if (tagBlockMatch.deprecated) {
251
- result.diagnostics.push(
252
- makeDgmoError(lineNum, `'## ${tagBlockMatch.name}' is no longer supported — use 'tag ${tagBlockMatch.name}' instead`)
253
- );
254
- continue;
255
- }
256
250
  currentTagGroup = {
257
251
  name: tagBlockMatch.name,
258
252
  alias: tagBlockMatch.alias,
@@ -27,10 +27,11 @@ const LEGACY_COLUMN_RE = /^==\s+(.+?)\s*(?:\[wip:\s*(\d+)\])?\s*==$/;
27
27
 
28
28
  /** Known kanban options (key-value). */
29
29
  const KNOWN_OPTIONS = new Set([
30
- 'color-off', 'hide',
30
+ 'hide',
31
31
  ]);
32
32
  /** Known kanban boolean options (bare keyword = on). */
33
33
  const KNOWN_BOOLEANS = new Set<string>([
34
+ 'no-auto-color',
34
35
  ]);
35
36
 
36
37
  // ============================================================
@@ -125,15 +126,11 @@ export function parseKanban(
125
126
  }
126
127
  }
127
128
 
128
- // Tag group heading — `tag: Name` (new) or `## Name` (deprecated)
129
- // Must be checked BEFORE OPTION_RE to prevent `tag: Rank` being swallowed as option
129
+ // Tag group heading — `tag Name`
130
+ // Must be checked BEFORE OPTION_RE to prevent `tag Rank` being swallowed as option
130
131
  if (!contentStarted) {
131
132
  const tagBlockMatch = matchTagBlockHeading(trimmed);
132
133
  if (tagBlockMatch) {
133
- if (tagBlockMatch.deprecated) {
134
- result.diagnostics.push(makeDgmoError(lineNumber, `'## ${tagBlockMatch.name}' is no longer supported — use 'tag: ${tagBlockMatch.name}' instead`));
135
- continue;
136
- }
137
134
  currentTagGroup = {
138
135
  name: tagBlockMatch.name,
139
136
  alias: tagBlockMatch.alias,
package/src/org/parser.ts CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  measureIndent,
8
8
  extractColor,
9
9
  parsePipeMetadata,
10
- MULTIPLE_PIPE_WARNING,
10
+ MULTIPLE_PIPE_ERROR,
11
11
  parseFirstLine,
12
12
  OPTION_NOCOLON_RE,
13
13
  } from '../utils/parsing';
@@ -46,18 +46,19 @@ const METADATA_RE = /^([^:]+):\s*(.+)$/;
46
46
 
47
47
  /** Known org chart options (key-value). */
48
48
  const KNOWN_OPTIONS = new Set([
49
- 'direction', 'sub-node-label', 'hide', 'show-sub-node-count',
49
+ 'sub-node-label', 'hide', 'show-sub-node-count',
50
50
  ]);
51
51
  /** Known org chart boolean options (bare keyword = on). */
52
52
  const KNOWN_BOOLEANS = new Set([
53
53
  'show-sub-node-count',
54
+ 'direction-tb',
54
55
  ]);
55
56
 
56
57
  // ============================================================
57
58
  // Inference
58
59
  // ============================================================
59
60
 
60
- /** Returns true if content contains tag group headings (`tag: …` or `## …`), suggesting an org chart. */
61
+ /** Returns true if content contains tag group headings (`tag …`), suggesting an org chart. */
61
62
  export function looksLikeOrg(content: string): boolean {
62
63
  for (const line of content.split('\n')) {
63
64
  const trimmed = line.trim();
@@ -169,10 +170,6 @@ export function parseOrg(
169
170
  pushError(lineNumber, 'Tag groups must appear before org content');
170
171
  continue;
171
172
  }
172
- if (tagBlockMatch.deprecated) {
173
- pushError(lineNumber, `'## ${tagBlockMatch.name}' is no longer supported — use 'tag: ${tagBlockMatch.name}' instead`);
174
- continue;
175
- }
176
173
  currentTagGroup = {
177
174
  name: tagBlockMatch.name,
178
175
  alias: tagBlockMatch.alias,
@@ -335,7 +332,7 @@ function parseNodeLabel(
335
332
  let rawLabel = segments[0];
336
333
  const { label, color } = extractColor(rawLabel, palette);
337
334
 
338
- const metadata = parsePipeMetadata(segments, aliasMap, warnFn ? () => warnFn(lineNumber, MULTIPLE_PIPE_WARNING) : undefined);
335
+ const metadata = parsePipeMetadata(segments, aliasMap, warnFn ? () => warnFn(lineNumber, MULTIPLE_PIPE_ERROR) : undefined);
339
336
 
340
337
  return {
341
338
  id: `node-${counter}`,
@@ -36,14 +36,14 @@ export interface ResolveImportsResult {
36
36
  const MAX_DEPTH = 10;
37
37
  const IMPORT_RE = /^(\s+)import:?\s+(.+\.dgmo)\s*$/i;
38
38
  const TAGS_RE = /^tags:?\s+(.+\.dgmo)\s*$/i;
39
- /** Matches new-style first line: `org ...` or old `chart: ...` or `title: ...` */
40
- const HEADER_RE = /^(org|kanban|chart\s*:|title\s*:)/i;
39
+ /** Matches new-style first line: `org ...` or `kanban ...` or `title: ...` */
40
+ const HEADER_RE = /^(org|kanban|title\s*:)/i;
41
41
  /**
42
42
  * Known option keys that can appear in org chart headers (space-separated).
43
43
  * Only these are stripped from imported files — avoids eating content like "Alice Chen".
44
44
  */
45
45
  const KNOWN_HEADER_OPTIONS = new Set([
46
- 'direction', 'sub-node-label', 'hide', 'show-sub-node-count',
46
+ 'direction-tb', 'sub-node-label', 'hide', 'show-sub-node-count',
47
47
  'color-off',
48
48
  ]);
49
49
 
package/src/render.ts CHANGED
@@ -36,8 +36,7 @@ async function ensureDom(): Promise<void> {
36
36
  * ```ts
37
37
  * import { render } from '@diagrammo/dgmo';
38
38
  *
39
- * const svg = await render(`chart: pie
40
- * title: Languages
39
+ * const svg = await render(`pie Languages
41
40
  * TypeScript: 45
42
41
  * Python: 30
43
42
  * Rust: 25`);
@@ -6,7 +6,7 @@ import { inferParticipantType } from './participant-inference';
6
6
  import type { DgmoError } from '../diagnostics';
7
7
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
8
8
  import { parseArrow } from '../utils/arrows';
9
- import { measureIndent, extractColor, parsePipeMetadata, MULTIPLE_PIPE_WARNING, parseFirstLine, OPTION_NOCOLON_RE } from '../utils/parsing';
9
+ import { measureIndent, extractColor, parsePipeMetadata, MULTIPLE_PIPE_ERROR, parseFirstLine, OPTION_NOCOLON_RE } from '../utils/parsing';
10
10
  import type { TagGroup } from '../utils/tag-groups';
11
11
  import { matchTagBlockHeading, validateTagValues } from '../utils/tag-groups';
12
12
 
@@ -185,17 +185,15 @@ const SECTION_PATTERN = /^==\s+(.+?)(?:\s*==)?\s*$/;
185
185
  // Arrow pattern for sequence inference — detects any arrow form
186
186
  const ARROW_PATTERN = /\S+\s*(?:<-\S+-|<~\S+~|-\S+->|~\S+~>|->|~>|<-|<~)\s*\S+/;
187
187
 
188
- // Note patterns — colon-free syntax
188
+ // Note patterns — colon-free syntax only
189
189
  // Single-line: "note text", "note left text", "note right of X text", "note left X text"
190
190
  // Multi-line: "note", "note right", "note right of X", "note left X" (body indented below)
191
- // Also supports legacy colon syntax: "note: text", "note right of X: text"
192
191
  //
193
192
  // The colon-free positioned form requires participant resolution — the parser
194
193
  // already has participant collection infrastructure, so we match the general
195
194
  // structure here and resolve participant vs text in the parsing logic.
196
- const NOTE_SINGLE_COLON = /^note(?:\s+(right|left)(?:\s+(?:of\s+)?(.+?))?)?\s*:\s*(.+)$/i;
197
195
  const NOTE_BARE = /^note\s+(.+)$/i;
198
- const NOTE_MULTI = /^note(?:\s+(right|left)(?:\s+(?:of\s+)?(.+?))?)?\s*:?\s*$/i;
196
+ const NOTE_MULTI = /^note(?:\s+(right|left)(?:\s+(?:of\s+)?(.+?))?)?\s*$/i;
199
197
 
200
198
  /** Result of parseNoteLine — indicates what the parser should do. */
201
199
  type NoteParseResult =
@@ -208,10 +206,10 @@ type NoteParseResult =
208
206
  * Parse a note line, resolving participant names from the known participants list.
209
207
  *
210
208
  * Supports:
211
- * - `note: text` / `note text` — default position (right), last msg sender
212
- * - `note left of X: text` / `note left of X text` / `note left X text`
213
- * - `note right:` / `note right` — multi-line head
214
- * - `note right of X:` / `note right of X` / `note left X` — multi-line head
209
+ * - `note text` — default position (right), last msg sender
210
+ * - `note left of X text` / `note left X text`
211
+ * - `note right` — multi-line head
212
+ * - `note right of X` / `note left X` — multi-line head
215
213
  * - Quoted participant: `note left "Auth Service" text`
216
214
  */
217
215
  function parseNoteLine(
@@ -222,25 +220,12 @@ function parseNoteLine(
222
220
  const lower = trimmed.toLowerCase();
223
221
  if (!lower.startsWith('note')) return null;
224
222
  // Must be exactly "note" or "note " — not "notebook" etc.
225
- if (trimmed.length > 4 && trimmed[4] !== ' ' && trimmed[4] !== ':') return null;
223
+ if (trimmed.length > 4 && trimmed[4] !== ' ') return null;
226
224
 
227
- // 1. Try legacy colon-based syntax first
228
- const colonMatch = trimmed.match(NOTE_SINGLE_COLON);
229
- if (colonMatch) {
230
- const position = (colonMatch[1]?.toLowerCase() as 'right' | 'left') || 'right';
231
- let participantId = colonMatch[2] || null;
232
- if (!participantId) {
233
- if (!lastMsgFrom) return { kind: 'skip' };
234
- participantId = lastMsgFrom;
235
- }
236
- if (!participants.some((p) => p.id === participantId)) return { kind: 'skip' };
237
- return { kind: 'single', position, participantId, text: colonMatch[3].trim() };
238
- }
239
-
240
- // 2. Try multi-line head (no text after note): `note`, `note right`, `note right of X`, `note left X`
225
+ // 1. Try multi-line head (no text after note): `note`, `note right`, `note right of X`, `note left X`
241
226
  // NOTE: NOTE_MULTI's (.+?) can greedily capture "participant text" as one group.
242
- // Only trust this match if the captured participant actually exists. Otherwise, fall
243
- // through to the bare-note handler which does proper participant-aware splitting.
227
+ // Only trust this match if the captured participant actually exists. Otherwise,
228
+ // fall through to the bare-note handler which does proper participant-aware splitting.
244
229
  const multiMatch = trimmed.match(NOTE_MULTI);
245
230
  if (multiMatch) {
246
231
  const position = (multiMatch[1]?.toLowerCase() as 'right' | 'left') || 'right';
@@ -255,7 +240,7 @@ function parseNoteLine(
255
240
  // Participant not found — fall through to bare-note handler for proper resolution
256
241
  }
257
242
 
258
- // 3. Bare note (colon-free): `note text` or `note left [of] X text`
243
+ // 2. Bare note: `note text` or `note left [of] X text`
259
244
  const bareMatch = trimmed.match(NOTE_BARE);
260
245
  if (bareMatch) {
261
246
  const rest = bareMatch[1].trim();
@@ -427,7 +412,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
427
412
  if (idx < 0) return { core: text };
428
413
  const core = text.substring(0, idx).trimEnd();
429
414
  const segments = text.substring(idx).split('|');
430
- const warnFn = ln != null ? () => pushWarning(ln, MULTIPLE_PIPE_WARNING) : undefined;
415
+ const warnFn = ln != null ? () => pushError(ln, MULTIPLE_PIPE_ERROR) : undefined;
431
416
  const meta = parsePipeMetadata(segments, aliasMap, warnFn);
432
417
  return Object.keys(meta).length > 0 ? { core, meta } : { core };
433
418
  };
@@ -475,7 +460,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
475
460
  const afterBracket = groupMatch[3]?.trim() || '';
476
461
  if (afterBracket.startsWith('|')) {
477
462
  const segments = afterBracket.split('|');
478
- const meta = parsePipeMetadata(segments, aliasMap, () => pushWarning(lineNumber, MULTIPLE_PIPE_WARNING));
463
+ const meta = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_ERROR));
479
464
  if (Object.keys(meta).length > 0) groupMeta = meta;
480
465
  }
481
466
 
@@ -531,9 +516,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
531
516
  }
532
517
 
533
518
  // ---- Tag group handling ----
534
- // Tag block heading: "tag: Name [alias X]"
519
+ // Tag block heading: "tag Name [alias X]"
535
520
  const tagBlockMatch = matchTagBlockHeading(trimmed);
536
- if (tagBlockMatch && !tagBlockMatch.deprecated) {
521
+ if (tagBlockMatch) {
537
522
  if (contentStarted) {
538
523
  pushError(lineNumber, 'Tag groups must appear before sequence content');
539
524
  continue;
@@ -608,14 +593,6 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
608
593
  } else {
609
594
  const value = trimmed.substring(colonIndex + 1).trim();
610
595
 
611
- if (key === 'chart') {
612
- hasExplicitChart = true;
613
- if (value.toLowerCase() !== 'sequence') {
614
- return fail(lineNumber, `Expected chart type "sequence", got "${value}"`);
615
- }
616
- continue;
617
- }
618
-
619
596
  // Enforce headers-before-content
620
597
  if (contentStarted) {
621
598
  pushError(lineNumber, `Options like '${key}: ${value}' must appear before the first message or declaration`);
@@ -666,8 +643,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
666
643
  }
667
644
 
668
645
  // Parse "Name is a type [aka Alias]" declarations (always top-level)
646
+ // Skip lines starting with 'note' — handled by note parsing below
669
647
  const { core: isACore, meta: isAMeta } = splitPipe(trimmed, lineNumber);
670
- const isAMatch = isACore.match(IS_A_PATTERN);
648
+ const isAMatch = !/^note(\s|$)/i.test(trimmed) ? isACore.match(IS_A_PATTERN) : null;
671
649
  if (isAMatch) {
672
650
  contentStarted = true;
673
651
  const id = isAMatch[1];
@@ -1049,12 +1027,11 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
1049
1027
  continue;
1050
1028
  }
1051
1029
 
1052
- // ---- Note parsing (colon-free + legacy colon syntax) ----
1030
+ // ---- Note parsing (space-separated only) ----
1053
1031
  // Strategy:
1054
- // 1. Try colon-based syntax: `note right of X: text` (legacy, still supported)
1055
- // 2. Try bare note: `note text` — position defaults, text is everything after `note`
1056
- // 3. For positioned: `note left [of] X text` needs participant lookup to split name vs text
1057
- // 4. Multi-line: `note`, `note right`, `note right [of] X` (body indented below)
1032
+ // 1. Try bare note: `note text` position defaults, text is everything after `note`
1033
+ // 2. For positioned: `note left [of] X text` — needs participant lookup to split name vs text
1034
+ // 3. Multi-line: `note`, `note right`, `note right [of] X` (body indented below)
1058
1035
  {
1059
1036
  const noteParsed = parseNoteLine(trimmed, result.participants, lastMsgFrom);
1060
1037
  if (noteParsed) {
@@ -11,10 +11,8 @@ import {
11
11
  measureIndent,
12
12
  extractColor,
13
13
  parsePipeMetadata,
14
- normalizeDirection,
15
14
  inferArrowColor,
16
- MULTIPLE_PIPE_WARNING,
17
- CHART_TYPE_RE,
15
+ MULTIPLE_PIPE_ERROR,
18
16
  TITLE_RE,
19
17
  OPTION_RE,
20
18
  parseFirstLine,
@@ -92,7 +90,7 @@ export function looksLikeSitemap(content: string): boolean {
92
90
  if (!trimmed || trimmed.startsWith('//')) continue;
93
91
 
94
92
  // Skip header lines
95
- if (CHART_TYPE_RE.test(trimmed) || TITLE_RE.test(trimmed)) continue;
93
+ if (parseFirstLine(trimmed) || TITLE_RE.test(trimmed)) continue;
96
94
  if (isTagBlockHeading(trimmed)) continue;
97
95
 
98
96
  if (/^-.*->\s*.+/.test(trimmed) || /^->\s*.+/.test(trimmed)) {
@@ -124,7 +122,7 @@ export function parseSitemap(
124
122
  const result: ParsedSitemap = {
125
123
  title: null,
126
124
  titleLineNumber: null,
127
- direction: 'TB',
125
+ direction: 'LR',
128
126
  roots: [],
129
127
  edges: [],
130
128
  tagGroups: [],
@@ -226,10 +224,6 @@ export function parseSitemap(
226
224
  pushError(lineNumber, 'Tag groups must appear before sitemap content');
227
225
  continue;
228
226
  }
229
- if (tagBlockMatch.deprecated) {
230
- pushError(lineNumber, `'## ${tagBlockMatch.name}' is no longer supported — use 'tag: ${tagBlockMatch.name}' instead`);
231
- continue;
232
- }
233
227
  currentTagGroup = {
234
228
  name: tagBlockMatch.name,
235
229
  alias: tagBlockMatch.alias,
@@ -247,16 +241,15 @@ export function parseSitemap(
247
241
  // Skip lines with `|` (pipe metadata) or `->` (arrows) — those are content
248
242
  if (!contentStarted && !currentTagGroup && measureIndent(line) === 0
249
243
  && !trimmed.includes('|') && !trimmed.includes('->')) {
244
+ // Bare boolean: direction-tb
245
+ if (/^direction-tb$/i.test(trimmed)) {
246
+ result.direction = 'TB';
247
+ continue;
248
+ }
249
+
250
250
  const optMatch = trimmed.match(OPTION_NOCOLON_RE);
251
251
  if (optMatch) {
252
252
  const key = optMatch[1].trim().toLowerCase();
253
- if (key === 'direction' || key === 'orientation') {
254
- const dir = normalizeDirection(optMatch[2]);
255
- if (dir) {
256
- result.direction = dir as SitemapDirection;
257
- }
258
- continue;
259
- }
260
253
  result.options[key] = optMatch[2].trim();
261
254
  continue;
262
255
  }
@@ -440,7 +433,7 @@ function parseNodeLabel(
440
433
  const segments = trimmed.split('|').map((s) => s.trim());
441
434
  const rawLabel = segments[0];
442
435
  const { label, color } = extractColor(rawLabel, palette);
443
- const metadata = parsePipeMetadata(segments, aliasMap, warnFn ? () => warnFn(lineNumber, MULTIPLE_PIPE_WARNING) : undefined);
436
+ const metadata = parsePipeMetadata(segments, aliasMap, warnFn ? () => warnFn(lineNumber, MULTIPLE_PIPE_ERROR) : undefined);
444
437
 
445
438
  return {
446
439
  id: `node-${counter}`,