@diagrammo/dgmo 0.30.0 → 0.32.0
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.
- package/.cursorrules +4 -1
- package/.github/copilot-instructions.md +4 -1
- package/.windsurfrules +4 -1
- package/README.md +21 -3
- package/SKILL.md +4 -1
- package/dist/advanced.cjs +1853 -623
- package/dist/advanced.d.cts +143 -16
- package/dist/advanced.d.ts +143 -16
- package/dist/advanced.js +1846 -623
- package/dist/auto.cjs +1640 -581
- package/dist/auto.js +99 -99
- package/dist/auto.mjs +1640 -581
- package/dist/cli.cjs +148 -147
- package/dist/index.cjs +1643 -662
- package/dist/index.js +1643 -662
- package/docs/ai-integration.md +4 -1
- package/docs/language-reference.md +282 -27
- package/gallery/fixtures/boxes-and-lines.dgmo +2 -2
- package/gallery/fixtures/c4-full.dgmo +4 -5
- package/gallery/fixtures/c4.dgmo +2 -3
- package/package.json +7 -1
- package/src/advanced.ts +10 -0
- package/src/boxes-and-lines/focus.ts +257 -0
- package/src/boxes-and-lines/layout-search.ts +345 -65
- package/src/boxes-and-lines/layout.ts +11 -1
- package/src/boxes-and-lines/parser.ts +97 -4
- package/src/boxes-and-lines/renderer.ts +111 -8
- package/src/boxes-and-lines/types.ts +9 -0
- package/src/c4/parser.ts +8 -7
- package/src/c4/renderer.ts +7 -5
- package/src/chart-type-registry.ts +129 -4
- package/src/chart-types.ts +3 -3
- package/src/chart.ts +18 -1
- package/src/class/renderer.ts +4 -2
- package/src/cli-banner.ts +107 -0
- package/src/cli.ts +13 -0
- package/src/colors.ts +247 -2
- package/src/cycle/parser.ts +2 -7
- package/src/d3.ts +67 -54
- package/src/diagnostics.ts +17 -0
- package/src/dimensions.ts +9 -13
- package/src/echarts.ts +42 -14
- package/src/er/parser.ts +6 -1
- package/src/er/renderer.ts +4 -2
- package/src/gantt/parser.ts +44 -7
- package/src/graph/flowchart-parser.ts +77 -3
- package/src/graph/flowchart-renderer.ts +4 -2
- package/src/graph/state-renderer.ts +6 -4
- package/src/infra/parser.ts +80 -0
- package/src/infra/renderer.ts +8 -4
- package/src/journey-map/parser.ts +23 -8
- package/src/journey-map/renderer.ts +1 -1
- package/src/kanban/parser.ts +8 -7
- package/src/kanban/renderer.ts +1 -1
- package/src/map/context-labels.ts +134 -27
- package/src/map/geo.ts +10 -2
- package/src/map/layout.ts +259 -4
- package/src/map/parser.ts +2 -0
- package/src/map/renderer.ts +49 -25
- package/src/map/resolver.ts +68 -19
- package/src/mindmap/parser.ts +15 -7
- package/src/mindmap/renderer.ts +55 -15
- package/src/org/parser.ts +8 -7
- package/src/org/renderer.ts +89 -127
- package/src/palettes/color-utils.ts +19 -4
- package/src/palettes/index.ts +1 -0
- package/src/pert/renderer.ts +15 -10
- package/src/pyramid/parser.ts +2 -7
- package/src/quadrant/renderer.ts +2 -2
- package/src/raci/parser.ts +2 -7
- package/src/raci/renderer.ts +5 -5
- package/src/ring/parser.ts +2 -7
- package/src/sequence/parser.ts +18 -7
- package/src/sequence/renderer.ts +4 -4
- package/src/sitemap/parser.ts +8 -7
- package/src/sitemap/renderer.ts +37 -39
- package/src/tech-radar/parser.ts +2 -7
- package/src/timeline/renderer.ts +15 -5
- package/src/utils/card.ts +183 -0
- package/src/utils/parsing.ts +13 -1
- package/src/utils/scaling.ts +38 -81
- package/src/utils/tag-groups.ts +48 -10
- package/src/utils/visual-conventions.ts +61 -0
- package/src/visualizations/parse.ts +6 -1
- package/src/wireframe/parser.ts +6 -1
package/src/er/parser.ts
CHANGED
|
@@ -377,7 +377,12 @@ export function parseERDiagram(
|
|
|
377
377
|
// Tag group entries (indented under tag heading)
|
|
378
378
|
if (currentTagGroup && !contentStarted && indent > 0) {
|
|
379
379
|
const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
|
|
380
|
-
const { label, color } = extractColor(
|
|
380
|
+
const { label, color } = extractColor(
|
|
381
|
+
cleanEntry,
|
|
382
|
+
palette,
|
|
383
|
+
result.diagnostics,
|
|
384
|
+
lineNumber
|
|
385
|
+
);
|
|
381
386
|
// Bare value (no explicit color) → keep it; finalized below.
|
|
382
387
|
if (isDefault) {
|
|
383
388
|
currentTagGroup.defaultValue = label;
|
package/src/er/renderer.ts
CHANGED
|
@@ -46,8 +46,10 @@ const MAX_SCALE = 3;
|
|
|
46
46
|
const TABLE_FONT_SIZE = 13;
|
|
47
47
|
const COLUMN_FONT_SIZE = 11;
|
|
48
48
|
const EDGE_LABEL_FONT_SIZE = 11;
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
import {
|
|
50
|
+
EDGE_STROKE_WIDTH,
|
|
51
|
+
NODE_STROKE_WIDTH,
|
|
52
|
+
} from '../utils/visual-conventions'; // shared (Story 111.1)
|
|
51
53
|
const MEMBER_LINE_HEIGHT = 18;
|
|
52
54
|
const COMPARTMENT_PADDING_Y = 8;
|
|
53
55
|
const MEMBER_PADDING_X = 10;
|
package/src/gantt/parser.ts
CHANGED
|
@@ -200,6 +200,8 @@ export function parseGantt(
|
|
|
200
200
|
syntaxMode: 'new', // always new-mode at 1.0
|
|
201
201
|
};
|
|
202
202
|
|
|
203
|
+
// Bespoke (not the shared makeFail, Story 111.4): gantt accumulates into a
|
|
204
|
+
// local `diagnostics` array, not `result.diagnostics`.
|
|
203
205
|
const fail = (line: number, message: string): ParsedGantt => {
|
|
204
206
|
const diag = makeDgmoError(line, message);
|
|
205
207
|
diagnostics.push(diag);
|
|
@@ -396,7 +398,12 @@ export function parseGantt(
|
|
|
396
398
|
if (eraEntryMatch) {
|
|
397
399
|
// Capture groups 1-3 guaranteed by successful regex match.
|
|
398
400
|
const eraLabelRaw = eraEntryMatch[3]!.trim();
|
|
399
|
-
const eraExtracted = extractColor(
|
|
401
|
+
const eraExtracted = extractColor(
|
|
402
|
+
eraLabelRaw,
|
|
403
|
+
palette,
|
|
404
|
+
diagnostics,
|
|
405
|
+
lineNumber
|
|
406
|
+
);
|
|
400
407
|
result.eras.push({
|
|
401
408
|
startDate: eraEntryMatch[1]!,
|
|
402
409
|
endDate: eraEntryMatch[2]!,
|
|
@@ -423,7 +430,12 @@ export function parseGantt(
|
|
|
423
430
|
if (markerEntryMatch) {
|
|
424
431
|
// Capture groups 1-2 guaranteed by successful regex match.
|
|
425
432
|
const markerLabelRaw = markerEntryMatch[2]!.trim();
|
|
426
|
-
const markerExtracted = extractColor(
|
|
433
|
+
const markerExtracted = extractColor(
|
|
434
|
+
markerLabelRaw,
|
|
435
|
+
palette,
|
|
436
|
+
diagnostics,
|
|
437
|
+
lineNumber
|
|
438
|
+
);
|
|
427
439
|
result.markers.push({
|
|
428
440
|
date: markerEntryMatch[1]!,
|
|
429
441
|
label: markerExtracted.label,
|
|
@@ -451,7 +463,12 @@ export function parseGantt(
|
|
|
451
463
|
// First entry is the default unless another is marked `default`
|
|
452
464
|
if (COMMENT_RE.test(line)) continue;
|
|
453
465
|
const { text: cleanEntry, isDefault } = stripDefaultModifier(line);
|
|
454
|
-
const extracted = extractColor(
|
|
466
|
+
const extracted = extractColor(
|
|
467
|
+
cleanEntry,
|
|
468
|
+
palette,
|
|
469
|
+
diagnostics,
|
|
470
|
+
lineNumber
|
|
471
|
+
);
|
|
455
472
|
const color =
|
|
456
473
|
extracted.color ||
|
|
457
474
|
seriesColors[currentTagGroup.entries.length % seriesColors.length] ||
|
|
@@ -710,7 +727,12 @@ export function parseGantt(
|
|
|
710
727
|
const startOff = parseOffsetPrefix('+' + eraOffsetMatch[1]!);
|
|
711
728
|
const endOff = parseOffsetPrefix('+' + eraOffsetMatch[2]!);
|
|
712
729
|
const eraLabelRaw = eraOffsetMatch[3]!.trim();
|
|
713
|
-
const eraExtracted = extractColor(
|
|
730
|
+
const eraExtracted = extractColor(
|
|
731
|
+
eraLabelRaw,
|
|
732
|
+
palette,
|
|
733
|
+
diagnostics,
|
|
734
|
+
lineNumber
|
|
735
|
+
);
|
|
714
736
|
result.eras.push({
|
|
715
737
|
startDate: '',
|
|
716
738
|
endDate: '',
|
|
@@ -728,7 +750,12 @@ export function parseGantt(
|
|
|
728
750
|
if (markerOffsetMatch) {
|
|
729
751
|
const dateOff = parseOffsetPrefix('+' + markerOffsetMatch[1]!);
|
|
730
752
|
const markerLabelRaw = markerOffsetMatch[2]!.trim();
|
|
731
|
-
const markerExtracted = extractColor(
|
|
753
|
+
const markerExtracted = extractColor(
|
|
754
|
+
markerLabelRaw,
|
|
755
|
+
palette,
|
|
756
|
+
diagnostics,
|
|
757
|
+
lineNumber
|
|
758
|
+
);
|
|
732
759
|
result.markers.push({
|
|
733
760
|
date: '',
|
|
734
761
|
label: markerExtracted.label,
|
|
@@ -825,7 +852,12 @@ export function parseGantt(
|
|
|
825
852
|
if (eraMatch) {
|
|
826
853
|
// Capture groups 1-3 guaranteed by successful regex match.
|
|
827
854
|
const eraLabelRaw = eraMatch[3]!.trim();
|
|
828
|
-
const eraExtracted = extractColor(
|
|
855
|
+
const eraExtracted = extractColor(
|
|
856
|
+
eraLabelRaw,
|
|
857
|
+
palette,
|
|
858
|
+
diagnostics,
|
|
859
|
+
lineNumber
|
|
860
|
+
);
|
|
829
861
|
result.eras.push({
|
|
830
862
|
startDate: eraMatch[1]!,
|
|
831
863
|
endDate: eraMatch[2]!,
|
|
@@ -848,7 +880,12 @@ export function parseGantt(
|
|
|
848
880
|
if (markerMatch) {
|
|
849
881
|
// Capture groups 1-2 guaranteed by successful regex match.
|
|
850
882
|
const markerLabelRaw = markerMatch[2]!.trim();
|
|
851
|
-
const markerExtracted = extractColor(
|
|
883
|
+
const markerExtracted = extractColor(
|
|
884
|
+
markerLabelRaw,
|
|
885
|
+
palette,
|
|
886
|
+
diagnostics,
|
|
887
|
+
lineNumber
|
|
888
|
+
);
|
|
852
889
|
result.markers.push({
|
|
853
890
|
date: markerMatch[1]!,
|
|
854
891
|
label: markerExtracted.label,
|
|
@@ -100,6 +100,29 @@ function parseNodeRef(text: string): NodeRef | null {
|
|
|
100
100
|
return null;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Match a leading shape token plus any trailing text. Used to SALVAGE a node
|
|
105
|
+
* that `parseNodeRef` rejects because of trailing junk — most commonly an
|
|
106
|
+
* unsupported tag/metadata suffix the AI emits (`(Denied) s: Denied`).
|
|
107
|
+
* Flowcharts have no metadata/tag-group system (color a node inline instead:
|
|
108
|
+
* `(Denied red)`), so rather than silently drop the node AND its edge we keep
|
|
109
|
+
* the node, strip the suffix, and let the caller warn. The shape regexes are
|
|
110
|
+
* non-greedy so the FIRST balanced delimiter wins.
|
|
111
|
+
*/
|
|
112
|
+
function parseNodeRefLoose(
|
|
113
|
+
text: string
|
|
114
|
+
): { ref: NodeRef; trailing: string } | null {
|
|
115
|
+
const t = text.trim();
|
|
116
|
+
// Order mirrors parseNodeRef: subroutine/document before process.
|
|
117
|
+
const shapeRe =
|
|
118
|
+
/^(\[\[.+?\]\]|\[.+?~\]|\[.+?\]|\(.+?\)|<.+?>|\/.+?\/)\s+(\S.*)$/;
|
|
119
|
+
const m = t.match(shapeRe);
|
|
120
|
+
if (!m) return null;
|
|
121
|
+
const ref = parseNodeRef(m[1]!);
|
|
122
|
+
if (!ref) return null;
|
|
123
|
+
return { ref, trailing: m[2]!.trim() };
|
|
124
|
+
}
|
|
125
|
+
|
|
103
126
|
/**
|
|
104
127
|
* Split a line into segments around arrow tokens.
|
|
105
128
|
* Arrows: `->`, `-label->`, and long-dash variants like `-->`, `--->`,
|
|
@@ -270,6 +293,11 @@ export function parseFlowchart(
|
|
|
270
293
|
const notes: GraphNote[] = [];
|
|
271
294
|
let contentStarted = false;
|
|
272
295
|
let firstLineParsed = false;
|
|
296
|
+
// The last node id seen on the PREVIOUS content line — the implicit source
|
|
297
|
+
// for a line that begins with a bare arrow (`(Start)` then `-> Next` on its
|
|
298
|
+
// own line). Without this, the same-indent pop empties the indent stack and
|
|
299
|
+
// the leading-arrow line is dropped, orphaning the prior node.
|
|
300
|
+
let prevLineLastNodeId: string | null = null;
|
|
273
301
|
|
|
274
302
|
// Per-parse alias literal → canonical node id (TD-18). Per C8:
|
|
275
303
|
// never persisted, fresh each parse.
|
|
@@ -287,6 +315,22 @@ export function parseFlowchart(
|
|
|
287
315
|
return { seg: m[1]!.trim(), alias: m[2]! };
|
|
288
316
|
}
|
|
289
317
|
|
|
318
|
+
// Lines we've already warned about an unsupported node suffix on, so a chain
|
|
319
|
+
// with several salvaged nodes doesn't emit duplicate warnings per line.
|
|
320
|
+
const suffixWarnedLines = new Set<number>();
|
|
321
|
+
function warnUnsupportedSuffix(lineNumber: number, trailing: string): void {
|
|
322
|
+
if (suffixWarnedLines.has(lineNumber)) return;
|
|
323
|
+
suffixWarnedLines.add(lineNumber);
|
|
324
|
+
result.diagnostics.push(
|
|
325
|
+
makeDgmoError(
|
|
326
|
+
lineNumber,
|
|
327
|
+
`Ignored unsupported text after a node shape: "${trailing}". Flowcharts have no tag groups or node metadata; node colors are assigned automatically by shape (e.g. terminals green/red, decisions yellow). Remove the suffix.`,
|
|
328
|
+
'warning',
|
|
329
|
+
'W_FLOWCHART_NODE_SUFFIX'
|
|
330
|
+
)
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
290
334
|
function getOrCreateNode(ref: NodeRef, lineNumber: number): GraphNode {
|
|
291
335
|
const key = ref.id;
|
|
292
336
|
const existing = nodeMap.get(key);
|
|
@@ -371,6 +415,13 @@ export function parseFlowchart(
|
|
|
371
415
|
// Split line into segments around arrows
|
|
372
416
|
const segments = splitArrows(trimmed);
|
|
373
417
|
|
|
418
|
+
// A line that begins with a bare arrow (first segment empty) is a
|
|
419
|
+
// continuation of the previous line's chain — its source is the last node
|
|
420
|
+
// on the previous content line when the indent stack offers nothing.
|
|
421
|
+
const startsWithArrow = segments.length >= 2 && segments[0]!.trim() === '';
|
|
422
|
+
const effectiveSource =
|
|
423
|
+
implicitSourceId ?? (startsWithArrow ? prevLineLastNodeId : null);
|
|
424
|
+
|
|
374
425
|
if (segments.length === 1) {
|
|
375
426
|
// Single node reference, no arrows. May carry an `as <alias>`
|
|
376
427
|
// postfix per TD-18.
|
|
@@ -383,6 +434,15 @@ export function parseFlowchart(
|
|
|
383
434
|
indentStack.push({ nodeId: node.id, indent });
|
|
384
435
|
return node.id;
|
|
385
436
|
}
|
|
437
|
+
// Salvage a shape with an unsupported trailing suffix (e.g. a tag/
|
|
438
|
+
// metadata assignment `(Denied) s: Denied`) — keep the node, warn.
|
|
439
|
+
const loose = parseNodeRefLoose(peeled.seg);
|
|
440
|
+
if (loose) {
|
|
441
|
+
warnUnsupportedSuffix(lineNumber, loose.trailing);
|
|
442
|
+
const node = getOrCreateNode(loose.ref, lineNumber);
|
|
443
|
+
indentStack.push({ nodeId: node.id, indent });
|
|
444
|
+
return node.id;
|
|
445
|
+
}
|
|
386
446
|
// Bare-token alias reference: `os` (no brackets) on its own line
|
|
387
447
|
// resolves to the canonical id and pushes onto the indent stack.
|
|
388
448
|
const aliasResolved = nameAliasMap.get(peeled.seg.trim());
|
|
@@ -431,18 +491,28 @@ export function parseFlowchart(
|
|
|
431
491
|
}
|
|
432
492
|
}
|
|
433
493
|
}
|
|
494
|
+
if (!ref) {
|
|
495
|
+
// Salvage a shape with an unsupported trailing suffix (most often a
|
|
496
|
+
// tag/metadata assignment like `(Denied) s: Denied`) so the node AND
|
|
497
|
+
// its edge survive instead of being silently dropped.
|
|
498
|
+
const loose = parseNodeRefLoose(peeled.seg);
|
|
499
|
+
if (loose) {
|
|
500
|
+
warnUnsupportedSuffix(lineNumber, loose.trailing);
|
|
501
|
+
ref = loose.ref;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
434
504
|
if (!ref) continue;
|
|
435
505
|
|
|
436
506
|
const node = getOrCreateNode(ref, lineNumber);
|
|
437
507
|
if (peeled.alias) nameAliasMap.set(peeled.alias, node.id);
|
|
438
508
|
|
|
439
509
|
if (pendingArrow !== null) {
|
|
440
|
-
const sourceId = lastNodeId ??
|
|
510
|
+
const sourceId = lastNodeId ?? effectiveSource;
|
|
441
511
|
if (sourceId) {
|
|
442
512
|
addEdge(sourceId, node.id, lineNumber, pendingArrow.label);
|
|
443
513
|
}
|
|
444
514
|
pendingArrow = null;
|
|
445
|
-
} else if (lastNodeId === null &&
|
|
515
|
+
} else if (lastNodeId === null && effectiveSource === null) {
|
|
446
516
|
// First node in chain, no arrow yet — just register
|
|
447
517
|
}
|
|
448
518
|
|
|
@@ -571,7 +641,11 @@ export function parseFlowchart(
|
|
|
571
641
|
}
|
|
572
642
|
|
|
573
643
|
// Content line (nodes and edges)
|
|
574
|
-
processContentLine(trimmed, lineNumber, indent);
|
|
644
|
+
const lastId = processContentLine(trimmed, lineNumber, indent);
|
|
645
|
+
// Remember this line's tail so a following bare-arrow line can attach to
|
|
646
|
+
// it (leading-arrow continuation). Keep the prior value when a line
|
|
647
|
+
// produces no node so blank/unparseable lines don't break the chain.
|
|
648
|
+
if (lastId) prevLineLastNodeId = lastId;
|
|
575
649
|
}
|
|
576
650
|
|
|
577
651
|
// Validation: no nodes found
|
|
@@ -36,8 +36,10 @@ const DIAGRAM_PADDING = 20;
|
|
|
36
36
|
const MAX_SCALE = 3;
|
|
37
37
|
const NODE_FONT_SIZE = 13;
|
|
38
38
|
const EDGE_LABEL_FONT_SIZE = 11;
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
import {
|
|
40
|
+
EDGE_STROKE_WIDTH,
|
|
41
|
+
NODE_STROKE_WIDTH,
|
|
42
|
+
} from '../utils/visual-conventions'; // shared (Story 111.1)
|
|
41
43
|
const ARROWHEAD_W = 10;
|
|
42
44
|
const ARROWHEAD_H = 7;
|
|
43
45
|
const IO_SKEW = 15;
|
|
@@ -7,7 +7,7 @@ import { appendArrowheadMarkers } from '../utils/arrow-markers';
|
|
|
7
7
|
import { fitDiagramToCanvas } from '../utils/fit-canvas';
|
|
8
8
|
import { FONT_FAMILY } from '../fonts';
|
|
9
9
|
import type { PaletteColors } from '../palettes';
|
|
10
|
-
import { contrastText, mix, shapeFill } from '../palettes/color-utils';
|
|
10
|
+
import { contrastText, mix, shapeFill, themeBaseBg } from '../palettes/color-utils';
|
|
11
11
|
import type { ParsedGraph } from './types';
|
|
12
12
|
import type { LayoutResult, LayoutNode } from './layout';
|
|
13
13
|
import { parseState } from './state-parser';
|
|
@@ -37,8 +37,10 @@ const MAX_SCALE = 3;
|
|
|
37
37
|
const NODE_FONT_SIZE = 13;
|
|
38
38
|
const EDGE_LABEL_FONT_SIZE = 11;
|
|
39
39
|
const GROUP_LABEL_FONT_SIZE = 11;
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
import {
|
|
41
|
+
EDGE_STROKE_WIDTH,
|
|
42
|
+
NODE_STROKE_WIDTH,
|
|
43
|
+
} from '../utils/visual-conventions'; // shared (Story 111.1)
|
|
42
44
|
const ARROWHEAD_W = 10;
|
|
43
45
|
const ARROWHEAD_H = 7;
|
|
44
46
|
const PSEUDOSTATE_RADIUS = 10;
|
|
@@ -209,7 +211,7 @@ export function renderState(
|
|
|
209
211
|
const gh = group.height + sGroupExtraPadding * 2 + sGroupLabelFontSize + 4;
|
|
210
212
|
|
|
211
213
|
const fillColor = group.color
|
|
212
|
-
? mix(group.color,
|
|
214
|
+
? mix(group.color, themeBaseBg(palette, isDark), 10)
|
|
213
215
|
: isDark
|
|
214
216
|
? palette.surface
|
|
215
217
|
: mix(palette.border, palette.bg, 30);
|
package/src/infra/parser.ts
CHANGED
|
@@ -1066,9 +1066,89 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
1066
1066
|
|
|
1067
1067
|
validateTagGroupNames(result.tagGroups, warn, setError);
|
|
1068
1068
|
|
|
1069
|
+
checkReachability(result);
|
|
1070
|
+
|
|
1069
1071
|
return result;
|
|
1070
1072
|
}
|
|
1071
1073
|
|
|
1074
|
+
/**
|
|
1075
|
+
* Reachability lint: an infra diagram traces request traffic flowing inward
|
|
1076
|
+
* from an `internet`/`edge` entry, so every node must have a directed path
|
|
1077
|
+
* back to an entry. A node nothing routes to carries no traffic — it's dead
|
|
1078
|
+
* and doesn't belong. Emits `warning`-severity diagnostics (never blocks
|
|
1079
|
+
* rendering). Mirrors the compute model: an edge targeting a `[Group]`
|
|
1080
|
+
* delivers to all of that group's children.
|
|
1081
|
+
*/
|
|
1082
|
+
function checkReachability(result: Writable<ParsedInfra>): void {
|
|
1083
|
+
if (result.nodes.length === 0) return;
|
|
1084
|
+
|
|
1085
|
+
const entries = result.nodes.filter((n) => n.isEdge);
|
|
1086
|
+
|
|
1087
|
+
// No entry at all: the rule can't be satisfied. One diagnostic, not N.
|
|
1088
|
+
if (entries.length === 0) {
|
|
1089
|
+
const line = result.titleLineNumber ?? result.nodes[0]?.lineNumber ?? 1;
|
|
1090
|
+
result.diagnostics.push(
|
|
1091
|
+
makeDgmoError(
|
|
1092
|
+
line,
|
|
1093
|
+
`Infra diagram has no 'internet' or 'edge' entry point — an infra diagram traces request traffic from an entry inward, so without one nothing carries traffic. Add an 'internet' or 'edge' node and route from it.`,
|
|
1094
|
+
'warning',
|
|
1095
|
+
'W_INFRA_NO_ENTRY'
|
|
1096
|
+
)
|
|
1097
|
+
);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// groupId -> child node ids (for expanding edges that target a [Group]).
|
|
1102
|
+
const groupChildMap = new Map<string, string[]>();
|
|
1103
|
+
for (const node of result.nodes) {
|
|
1104
|
+
if (node.groupId) {
|
|
1105
|
+
const list = groupChildMap.get(node.groupId) ?? [];
|
|
1106
|
+
list.push(node.id);
|
|
1107
|
+
// node.groupId is already a normalized id from parse time.
|
|
1108
|
+
// eslint-disable-next-line name-normalize/required-at-insertion
|
|
1109
|
+
groupChildMap.set(node.groupId, list);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Outbound adjacency: an edge to a group reaches every child of that group.
|
|
1114
|
+
const outbound = new Map<string, string[]>();
|
|
1115
|
+
for (const edge of result.edges) {
|
|
1116
|
+
const targets = groupChildMap.get(edge.targetId) ?? [edge.targetId];
|
|
1117
|
+
const list = outbound.get(edge.sourceId) ?? [];
|
|
1118
|
+
list.push(...targets);
|
|
1119
|
+
outbound.set(edge.sourceId, list);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// BFS from every entry node.
|
|
1123
|
+
const reachable = new Set<string>();
|
|
1124
|
+
const queue: string[] = [];
|
|
1125
|
+
for (const entry of entries) {
|
|
1126
|
+
reachable.add(entry.id);
|
|
1127
|
+
queue.push(entry.id);
|
|
1128
|
+
}
|
|
1129
|
+
while (queue.length > 0) {
|
|
1130
|
+
const current = queue.shift()!;
|
|
1131
|
+
for (const next of outbound.get(current) ?? []) {
|
|
1132
|
+
if (!reachable.has(next)) {
|
|
1133
|
+
reachable.add(next);
|
|
1134
|
+
queue.push(next);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
for (const node of result.nodes) {
|
|
1140
|
+
if (node.isEdge || reachable.has(node.id)) continue;
|
|
1141
|
+
result.diagnostics.push(
|
|
1142
|
+
makeDgmoError(
|
|
1143
|
+
node.lineNumber,
|
|
1144
|
+
`'${node.label}' is unreachable from an 'internet'/'edge' entry — no request traffic flows to it, so it's dead on an infra diagram. Connect it downstream of an entry, or remove it.`,
|
|
1145
|
+
'warning',
|
|
1146
|
+
'W_INFRA_UNREACHABLE'
|
|
1147
|
+
)
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1072
1152
|
// ============================================================
|
|
1073
1153
|
// Symbol extraction (for completion API)
|
|
1074
1154
|
// ============================================================
|
package/src/infra/renderer.ts
CHANGED
|
@@ -57,20 +57,24 @@ import { ScaleContext } from '../utils/scaling';
|
|
|
57
57
|
// ============================================================
|
|
58
58
|
|
|
59
59
|
const NODE_FONT_SIZE = 13;
|
|
60
|
+
// Intentional deviation (conventions §1): infra uses denser meta rows
|
|
61
|
+
// (10px font / 14px line height) than the 11/16 default.
|
|
60
62
|
const META_FONT_SIZE = 10;
|
|
61
63
|
const META_LINE_HEIGHT = 14;
|
|
62
64
|
const EDGE_LABEL_FONT_SIZE = 11;
|
|
63
65
|
const GROUP_LABEL_FONT_SIZE = 14;
|
|
64
66
|
const NODE_BORDER_RADIUS = 8;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
import {
|
|
68
|
+
EDGE_STROKE_WIDTH,
|
|
69
|
+
NODE_STROKE_WIDTH,
|
|
70
|
+
COLLAPSE_BAR_HEIGHT,
|
|
71
|
+
COLLAPSE_BAR_INSET,
|
|
72
|
+
} from '../utils/visual-conventions'; // shared (Story 111.1)
|
|
67
73
|
const OVERLOAD_STROKE_WIDTH = 3;
|
|
68
74
|
const ROLE_DOT_RADIUS = 3;
|
|
69
75
|
const NODE_HEADER_HEIGHT = 28;
|
|
70
76
|
const NODE_SEPARATOR_GAP = 4;
|
|
71
77
|
const NODE_PAD_BOTTOM = 10;
|
|
72
|
-
const COLLAPSE_BAR_HEIGHT = 6;
|
|
73
|
-
const COLLAPSE_BAR_INSET = 0;
|
|
74
78
|
|
|
75
79
|
const LEGEND_FIXED_GAP = 16; // gap between fixed legend and scaled diagram — local, not shared
|
|
76
80
|
const SPEED_BADGE_H_PAD = 5; // horizontal padding inside active speed badge
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
formatDgmoError,
|
|
6
6
|
journeyBareScoreRemovedMessage,
|
|
7
7
|
makeDgmoError,
|
|
8
|
+
makeFail,
|
|
8
9
|
METADATA_DIAGNOSTIC_CODES,
|
|
9
10
|
pipeOperatorRemovedMessage,
|
|
10
11
|
suggest,
|
|
@@ -75,12 +76,7 @@ export function parseJourneyMap(
|
|
|
75
76
|
error: null,
|
|
76
77
|
};
|
|
77
78
|
|
|
78
|
-
const fail = (
|
|
79
|
-
const diag = makeDgmoError(line, message);
|
|
80
|
-
result.diagnostics.push(diag);
|
|
81
|
-
result.error = formatDgmoError(diag);
|
|
82
|
-
return result;
|
|
83
|
-
};
|
|
79
|
+
const fail = makeFail(result);
|
|
84
80
|
|
|
85
81
|
const warn = (line: number, message: string): void => {
|
|
86
82
|
result.diagnostics.push(makeDgmoError(line, message, 'warning'));
|
|
@@ -184,7 +180,21 @@ export function parseJourneyMap(
|
|
|
184
180
|
}
|
|
185
181
|
}
|
|
186
182
|
} else {
|
|
187
|
-
|
|
183
|
+
// Same-line form (pipes removed in 0.18.0): peel a trailing
|
|
184
|
+
// `color: <token>` off the rest-of-line persona name.
|
|
185
|
+
const colorMatch = afterKeyword.match(/^(.+?)\s+color:\s*(\S+)$/i);
|
|
186
|
+
if (colorMatch) {
|
|
187
|
+
personaName = colorMatch[1]!.trim();
|
|
188
|
+
personaColor =
|
|
189
|
+
resolveColorWithDiagnostic(
|
|
190
|
+
colorMatch[2]!,
|
|
191
|
+
lineNumber,
|
|
192
|
+
result.diagnostics,
|
|
193
|
+
palette
|
|
194
|
+
) ?? undefined;
|
|
195
|
+
} else {
|
|
196
|
+
personaName = afterKeyword;
|
|
197
|
+
}
|
|
188
198
|
}
|
|
189
199
|
|
|
190
200
|
if (!personaName) {
|
|
@@ -252,7 +262,12 @@ export function parseJourneyMap(
|
|
|
252
262
|
if (currentTagGroup && !contentStarted) {
|
|
253
263
|
if (indent > 0) {
|
|
254
264
|
const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
|
|
255
|
-
const { label, color } = extractColor(
|
|
265
|
+
const { label, color } = extractColor(
|
|
266
|
+
cleanEntry,
|
|
267
|
+
palette,
|
|
268
|
+
result.diagnostics,
|
|
269
|
+
lineNumber
|
|
270
|
+
);
|
|
256
271
|
// Bare value (no explicit color) → keep it; finalized below.
|
|
257
272
|
if (isDefault) {
|
|
258
273
|
currentTagGroup.defaultValue = label;
|
|
@@ -52,7 +52,7 @@ export interface JourneyMapInteractiveOptions {
|
|
|
52
52
|
// Match kanban styling constants
|
|
53
53
|
const DIAGRAM_PADDING = 20;
|
|
54
54
|
const PADDING = DIAGRAM_PADDING;
|
|
55
|
-
|
|
55
|
+
import { CARD_RADIUS } from '../utils/visual-conventions'; // shared (Story 111.1)
|
|
56
56
|
const CARD_PADDING_X = 10;
|
|
57
57
|
const CARD_PADDING_Y = 6;
|
|
58
58
|
const CARD_HEADER_HEIGHT = 24;
|
package/src/kanban/parser.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { PaletteColors } from '../palettes';
|
|
|
2
2
|
import {
|
|
3
3
|
formatDgmoError,
|
|
4
4
|
makeDgmoError,
|
|
5
|
+
makeFail,
|
|
5
6
|
METADATA_DIAGNOSTIC_CODES,
|
|
6
7
|
pipeOperatorRemovedMessage,
|
|
7
8
|
suggest,
|
|
@@ -77,12 +78,7 @@ export function parseKanban(
|
|
|
77
78
|
error: null,
|
|
78
79
|
};
|
|
79
80
|
|
|
80
|
-
const fail = (
|
|
81
|
-
const diag = makeDgmoError(line, message);
|
|
82
|
-
result.diagnostics.push(diag);
|
|
83
|
-
result.error = formatDgmoError(diag);
|
|
84
|
-
return result;
|
|
85
|
-
};
|
|
81
|
+
const fail = makeFail(result);
|
|
86
82
|
|
|
87
83
|
const warn = (line: number, message: string): void => {
|
|
88
84
|
result.diagnostics.push(makeDgmoError(line, message, 'warning'));
|
|
@@ -218,7 +214,12 @@ export function parseKanban(
|
|
|
218
214
|
const indent = measureIndent(line);
|
|
219
215
|
if (indent > 0) {
|
|
220
216
|
const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
|
|
221
|
-
const { label, color } = extractColor(
|
|
217
|
+
const { label, color } = extractColor(
|
|
218
|
+
cleanEntry,
|
|
219
|
+
palette,
|
|
220
|
+
result.diagnostics,
|
|
221
|
+
lineNumber
|
|
222
|
+
);
|
|
222
223
|
// Bare value (no explicit color) → keep it; finalized below.
|
|
223
224
|
if (isDefault) {
|
|
224
225
|
currentTagGroup.defaultValue = label;
|
package/src/kanban/renderer.ts
CHANGED
|
@@ -55,7 +55,7 @@ const CARD_HEADER_HEIGHT = 24;
|
|
|
55
55
|
const CARD_META_LINE_HEIGHT = 14;
|
|
56
56
|
const CARD_SEPARATOR_GAP = 4;
|
|
57
57
|
const CARD_GAP = 8;
|
|
58
|
-
|
|
58
|
+
import { CARD_RADIUS } from '../utils/visual-conventions'; // shared (Story 111.1)
|
|
59
59
|
const CARD_PADDING_X = 10;
|
|
60
60
|
const CARD_PADDING_Y = 6;
|
|
61
61
|
const CARD_STROKE_WIDTH = 1.5;
|