@diagrammo/dgmo 0.16.0 → 0.17.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.
Files changed (53) hide show
  1. package/dist/advanced.cjs +133 -280
  2. package/dist/advanced.d.cts +9 -2
  3. package/dist/advanced.d.ts +9 -2
  4. package/dist/advanced.js +133 -280
  5. package/dist/auto.cjs +135 -269
  6. package/dist/auto.js +96 -96
  7. package/dist/auto.mjs +135 -269
  8. package/dist/cli.cjs +125 -125
  9. package/dist/index.cjs +132 -266
  10. package/dist/index.js +132 -266
  11. package/dist/internal.cjs +133 -280
  12. package/dist/internal.d.cts +9 -2
  13. package/dist/internal.d.ts +9 -2
  14. package/dist/internal.js +133 -280
  15. package/docs/language-reference.md +14 -18
  16. package/docs/migration-sequence-color-to-tags.md +1 -1
  17. package/gallery/fixtures/sequence-tags-protocols.dgmo +3 -3
  18. package/gallery/fixtures/sequence-tags.dgmo +3 -3
  19. package/gallery/fixtures/sequence.dgmo +4 -4
  20. package/package.json +7 -3
  21. package/src/auto/index.ts +2 -2
  22. package/src/boxes-and-lines/layout.ts +1 -2
  23. package/src/c4/parser.ts +1 -1
  24. package/src/class/parser.ts +1 -1
  25. package/src/cli.ts +2 -2
  26. package/src/completion.ts +1 -14
  27. package/src/cycle/parser.ts +1 -1
  28. package/src/d3.ts +2 -3
  29. package/src/diagnostics.ts +20 -0
  30. package/src/echarts.ts +2 -2
  31. package/src/editor/dgmo.grammar.d.ts +1 -1
  32. package/src/er/parser.ts +1 -1
  33. package/src/graph/flowchart-renderer.ts +3 -0
  34. package/src/infra/renderer.ts +1 -2
  35. package/src/journey-map/parser.ts +1 -1
  36. package/src/kanban/parser.ts +1 -1
  37. package/src/mindmap/parser.ts +2 -3
  38. package/src/org/parser.ts +1 -1
  39. package/src/pert/analyzer.ts +10 -10
  40. package/src/pert/layout.ts +1 -1
  41. package/src/pert/parser.ts +1 -1
  42. package/src/pyramid/parser.ts +1 -1
  43. package/src/raci/parser.ts +2 -2
  44. package/src/ring/parser.ts +1 -1
  45. package/src/sequence/parser.ts +66 -14
  46. package/src/sequence/participant-inference.ts +18 -181
  47. package/src/sequence/renderer.ts +47 -136
  48. package/src/sitemap/parser.ts +1 -1
  49. package/src/tech-radar/parser.ts +2 -2
  50. package/src/utils/extract-alias.ts +1 -1
  51. package/src/utils/inline-markdown.ts +1 -1
  52. package/src/utils/time-ticks.ts +1 -1
  53. package/src/wireframe/parser.ts +1 -1
@@ -11,6 +11,7 @@ import {
11
11
  NAME_DIAGNOSTIC_CODES,
12
12
  nameMergedMessage,
13
13
  akaRemovedMessage,
14
+ participantTypeRemovedMessage,
14
15
  } from '../diagnostics';
15
16
  import { normalizeName, displayName } from '../utils/name-normalize';
16
17
  import { parseArrow, parseInArrowLabel } from '../utils/arrows';
@@ -60,29 +61,40 @@ const KNOWN_SEQ_BOOLEANS = new Set(['activations', 'solid-fill', 'no-title']);
60
61
 
61
62
  /**
62
63
  * Participant types that can be declared via "Name is a type" syntax.
64
+ *
65
+ * The 0.16.0 trim retained only the types whose shapes carry semantic
66
+ * weight at a glance: stick figure (actor), cylinder (database),
67
+ * dashed cylinder (cache), horizontal pipe (queue), plus the default
68
+ * rectangle. The legacy `service`/`frontend`/`networking`/`gateway`/
69
+ * `external` keywords are rejected at parse time via
70
+ * `E_PARTICIPANT_TYPE_REMOVED`.
63
71
  */
64
72
  export type ParticipantType =
65
73
  | 'default'
66
- | 'service'
67
74
  | 'database'
68
75
  | 'actor'
69
76
  | 'queue'
70
- | 'cache'
71
- | 'gateway'
72
- | 'external'
73
- | 'networking'
74
- | 'frontend';
77
+ | 'cache';
75
78
 
76
79
  const VALID_PARTICIPANT_TYPES: ReadonlySet<string> = new Set([
77
- 'service',
78
80
  'database',
79
81
  'actor',
80
82
  'queue',
81
83
  'cache',
84
+ ]);
85
+
86
+ /**
87
+ * Participant-type keywords that were removed in 0.16.0. Used to
88
+ * gate `is a X` declarations with a hard parse error rather than
89
+ * silent fall-through to default — aligns with the pre-1.0
90
+ * break-and-bump policy (see `E_AKA_REMOVED` precedent).
91
+ */
92
+ const REMOVED_PARTICIPANT_TYPES: ReadonlySet<string> = new Set([
93
+ 'service',
94
+ 'frontend',
95
+ 'networking',
82
96
  'gateway',
83
97
  'external',
84
- 'networking',
85
- 'frontend',
86
98
  ]);
87
99
 
88
100
  /**
@@ -205,9 +217,9 @@ export interface ParsedSequenceDgmo {
205
217
  error: string | null;
206
218
  }
207
219
 
208
- // "Name is a type" pattern — e.g. "Auth Server is a service"
220
+ // "Name is a type" pattern — e.g. "Auth Server is a database"
209
221
  // Participant names may contain spaces; [^:]+? stops at colons so that
210
- // note lines like "note right of A: this is a service" are not falsely matched.
222
+ // note lines like "note right of A: this is an actor" are not falsely matched.
211
223
  // Remainder after type is parsed separately for `position N` modifier.
212
224
  const IS_A_PATTERN = /^([^:]+?)\s+is\s+an?\s+(\w+)(?:\s+(.+))?$/i;
213
225
 
@@ -480,7 +492,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
480
492
  result.diagnostics.push(makeDgmoError(line, message, 'warning'));
481
493
  };
482
494
 
483
- if (!content || !content.trim()) {
495
+ if (!content?.trim()) {
484
496
  return fail(0, 'Empty content');
485
497
  }
486
498
 
@@ -494,7 +506,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
494
506
  const fl = lines[fi].trim();
495
507
  if (!fl || fl.startsWith('//')) continue;
496
508
  const parsed = parseFirstLine(fl);
497
- if (parsed && parsed.chartType === 'sequence') {
509
+ if (parsed?.chartType === 'sequence') {
498
510
  hasExplicitChart = true;
499
511
  firstLineIndex = fi;
500
512
  if (parsed.title) {
@@ -936,6 +948,23 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
936
948
  const typeStr = isAMatch[2].toLowerCase();
937
949
  let remainder = isAMatch[3]?.trim() || '';
938
950
 
951
+ // Reject the 5 removed-type keywords with a hard parse error
952
+ // (per Decision #2 / break-and-bump policy). Skip participant
953
+ // registration for the bad line — downstream message references
954
+ // will surface a "participant not declared" diagnostic, which
955
+ // is the intended behavior.
956
+ if (REMOVED_PARTICIPANT_TYPES.has(typeStr)) {
957
+ result.diagnostics.push(
958
+ makeDgmoError(
959
+ lineNumber,
960
+ participantTypeRemovedMessage(typeStr),
961
+ 'error',
962
+ NAME_DIAGNOSTIC_CODES.PARTICIPANT_TYPE_REMOVED
963
+ )
964
+ );
965
+ continue;
966
+ }
967
+
939
968
  const participantType: ParticipantType = VALID_PARTICIPANT_TYPES.has(
940
969
  typeStr
941
970
  )
@@ -957,7 +986,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
957
986
  // register it. Order on the line is `Name is a TYPE [position N] as <alias>`.
958
987
  // The leading `(.*?)\s*\b` allows the remainder to be just
959
988
  // `as <alias>` (empty prefix) — the canonical example writes
960
- // `Alice is a service as a` where the entire remainder is `as a`.
989
+ // `Alice is an actor as a` where the entire remainder is `as a`.
961
990
  const asInRemainder = remainder.match(
962
991
  /^(.*?)\s*\bas\s+([A-Za-z][A-Za-z0-9_]{0,11})\s*$/
963
992
  );
@@ -1316,6 +1345,29 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
1316
1345
  continue;
1317
1346
  }
1318
1347
 
1348
+ // 'elif <label>' → suggest 'else if <label>'
1349
+ const elifMatch = trimmed.match(/^elif\b\s*(.*)$/i);
1350
+ if (elifMatch) {
1351
+ const tailRaw = elifMatch[1].trim();
1352
+ const tail = tailRaw ? ' ' + tailRaw : '';
1353
+ pushError(
1354
+ lineNumber,
1355
+ `'elif' is not a keyword. Did you mean 'else if${tail}'?`
1356
+ );
1357
+ continue;
1358
+ }
1359
+
1360
+ // 'else <text>' (where text isn't 'if …') → suggest 'else if <text>'
1361
+ const elseLabelMatch = trimmed.match(/^else\s+(.+)$/i);
1362
+ if (elseLabelMatch) {
1363
+ const tail = elseLabelMatch[1].trim();
1364
+ pushError(
1365
+ lineNumber,
1366
+ `'else' does not take a label. Did you mean 'else if ${tail}'?`
1367
+ );
1368
+ continue;
1369
+ }
1370
+
1319
1371
  // ---- Note parsing (space-separated only) ----
1320
1372
  // Strategy:
1321
1373
  // 1. Try bare note: `note text` — position defaults, text is everything after `note`
@@ -3,9 +3,14 @@
3
3
  // ============================================================
4
4
  //
5
5
  // Data-driven rules table that infers participant type from name.
6
- // First match wins. Infrastructure overrides come before suffix
7
- // rules to prevent false positives (e.g. "Router" networking,
8
- // not actor despite the "-er" suffix).
6
+ // First match wins. Conflict overrides come before suffix rules
7
+ // to prevent false positives within the surviving 4-type taxonomy.
8
+ //
9
+ // Surviving types: actor, database, cache, queue (plus default).
10
+ // Removed in 0.16.0: service, frontend, networking, gateway, external.
11
+ // Names that previously inferred to a removed type fall through to
12
+ // default (sharp rectangle). Inference is intentionally conservative
13
+ // — narrow product/name lists, no broad suffix catch-alls.
9
14
  // ============================================================
10
15
 
11
16
  import type { ParticipantType } from './parser';
@@ -23,95 +28,17 @@ interface InferenceRule {
23
28
  *
24
29
  * Priority order:
25
30
  * 0. Conflict overrides (prevent misclassification by general patterns)
26
- * 1. Infrastructure overrides (prevent false actor matches)
27
- * 2. Networking patterns
28
- * 3. Database patterns
29
- * 4. Cache patterns
30
- * 5. Queue/Messaging patterns
31
- * 6. Actor patterns (suffix + exact)
32
- * 7. Frontend patterns
33
- * 8. Service patterns
34
- * 9. External patterns
31
+ * 1. Database patterns
32
+ * 2. Cache patterns
33
+ * 3. Queue/Messaging patterns
34
+ * 4. Actor patterns (suffix + exact)
35
35
  */
36
36
  const PARTICIPANT_RULES: readonly InferenceRule[] = [
37
37
  // ── 0. Conflict overrides ────────────────────────────────
38
38
  // Names that would incorrectly match general patterns in later groups
39
39
  { pattern: /^KeyDB$/i, type: 'cache' }, // not database (DB$ suffix)
40
- { pattern: /Webhook/i, type: 'external' }, // not frontend (Web contains)
41
- { pattern: /^Upstream$/i, type: 'external' }, // not queue (Stream$ suffix)
42
- { pattern: /^Downstream$/i, type: 'external' }, // not queue (Stream$ suffix)
43
-
44
- // ── 1. Infrastructure overrides ─────────────────────────
45
- // These names end in -er/-or but are NOT actors
46
- { pattern: /^.*Router$/i, type: 'networking' },
47
- { pattern: /^.*Scheduler$/i, type: 'service' },
48
- { pattern: /^.*Dispatcher$/i, type: 'service' },
49
- { pattern: /^.*Balancer$/i, type: 'networking' },
50
- { pattern: /^.*Controller$/i, type: 'service' },
51
- { pattern: /^.*Handler$/i, type: 'service' },
52
- { pattern: /^.*Processor$/i, type: 'service' },
53
- { pattern: /^.*Connector$/i, type: 'service' },
54
- { pattern: /^.*Adapter$/i, type: 'service' },
55
- { pattern: /^.*Provider$/i, type: 'service' },
56
- { pattern: /^.*Manager$/i, type: 'service' },
57
- { pattern: /^.*Orchestrator$/i, type: 'service' },
58
- { pattern: /^.*Monitor$/i, type: 'service' },
59
- { pattern: /^.*Resolver$/i, type: 'service' },
60
- { pattern: /^.*Logger$/i, type: 'service' },
61
- { pattern: /^.*Server$/i, type: 'service' },
62
- { pattern: /^.*Broker$/i, type: 'queue' },
63
- { pattern: /^.*Worker$/i, type: 'service' },
64
- { pattern: /^.*Consumer$/i, type: 'service' },
65
- { pattern: /^.*Producer$/i, type: 'service' },
66
- { pattern: /^.*Publisher$/i, type: 'service' },
67
- { pattern: /^.*Subscriber$/i, type: 'service' },
68
- { pattern: /^.*Listener$/i, type: 'service' },
69
- // New -er/-or suffixes that are services, not actors
70
- { pattern: /^.*Watcher$/i, type: 'service' },
71
- { pattern: /^.*Executor$/i, type: 'service' },
72
- { pattern: /^.*Aggregator$/i, type: 'service' },
73
- { pattern: /^.*Collector$/i, type: 'service' },
74
- { pattern: /^.*Transformer$/i, type: 'service' },
75
- { pattern: /^.*Validator$/i, type: 'service' },
76
- { pattern: /^.*Generator$/i, type: 'service' },
77
- { pattern: /^.*Indexer$/i, type: 'service' },
78
- { pattern: /^.*Crawler$/i, type: 'service' },
79
- { pattern: /^.*Scanner$/i, type: 'service' },
80
- { pattern: /^.*Parser$/i, type: 'service' },
81
- { pattern: /^.*Emitter$/i, type: 'service' },
82
- { pattern: /^.*Exporter$/i, type: 'service' },
83
- { pattern: /^.*Importer$/i, type: 'service' },
84
- { pattern: /^.*Loader$/i, type: 'service' },
85
- { pattern: /^.*Renderer$/i, type: 'service' },
86
- { pattern: /^.*Checker$/i, type: 'service' },
87
- { pattern: /^.*Inspector$/i, type: 'service' },
88
- { pattern: /^.*Encoder$/i, type: 'service' },
89
- { pattern: /^.*Decoder$/i, type: 'service' },
90
- { pattern: /^.*Notifier$/i, type: 'service' },
91
-
92
- // ── 2. Networking patterns ──────────────────────────────
93
- { pattern: /Gateway/i, type: 'networking' },
94
- { pattern: /GW$/i, type: 'networking' },
95
- { pattern: /Proxy/i, type: 'networking' },
96
- { pattern: /LB$/i, type: 'networking' },
97
- { pattern: /LoadBalancer/i, type: 'networking' },
98
- { pattern: /CDN/i, type: 'networking' },
99
- { pattern: /Firewall/i, type: 'networking' },
100
- { pattern: /WAF$/i, type: 'networking' },
101
- { pattern: /DNS/i, type: 'networking' },
102
- { pattern: /Ingress/i, type: 'networking' },
103
- // Named products & patterns
104
- { pattern: /Nginx/i, type: 'networking' },
105
- { pattern: /Traefik/i, type: 'networking' },
106
- { pattern: /Envoy/i, type: 'networking' },
107
- { pattern: /Istio/i, type: 'networking' },
108
- { pattern: /Kong/i, type: 'networking' },
109
- { pattern: /Akamai/i, type: 'networking' },
110
- { pattern: /Cloudflare/i, type: 'networking' },
111
- { pattern: /Mesh$/i, type: 'networking' },
112
- { pattern: /ServiceMesh/i, type: 'networking' },
113
40
 
114
- // ── 3. Database patterns ────────────────────────────────
41
+ // ── 1. Database patterns ────────────────────────────────
115
42
  { pattern: /DB$/i, type: 'database' },
116
43
  { pattern: /Database/i, type: 'database' },
117
44
  { pattern: /Datastore/i, type: 'database' },
@@ -146,17 +73,16 @@ const PARTICIPANT_RULES: readonly InferenceRule[] = [
146
73
  { pattern: /Presto/i, type: 'database' },
147
74
  { pattern: /Table$/i, type: 'database' },
148
75
 
149
- // ── 4. Cache patterns ──────────────────────────────────
76
+ // ── 2. Cache patterns ──────────────────────────────────
150
77
  { pattern: /Cache/i, type: 'cache' },
151
78
  { pattern: /Redis/i, type: 'cache' },
152
79
  { pattern: /Memcache/i, type: 'cache' },
153
- // CDN already matched by networking above
154
80
  // Named products
155
81
  { pattern: /Dragonfly/i, type: 'cache' },
156
82
  { pattern: /Hazelcast/i, type: 'cache' },
157
83
  { pattern: /Valkey/i, type: 'cache' },
158
84
 
159
- // ── 5. Queue/Messaging patterns ─────────────────────────
85
+ // ── 3. Queue/Messaging patterns ─────────────────────────
160
86
  { pattern: /Queue/i, type: 'queue' },
161
87
  { pattern: /MQ$/i, type: 'queue' },
162
88
  { pattern: /SQS/i, type: 'queue' },
@@ -169,6 +95,7 @@ const PARTICIPANT_RULES: readonly InferenceRule[] = [
169
95
  { pattern: /Stream$/i, type: 'queue' },
170
96
  { pattern: /SNS/i, type: 'queue' },
171
97
  { pattern: /PubSub/i, type: 'queue' },
98
+ { pattern: /Broker$/i, type: 'queue' },
172
99
  // Named products & patterns
173
100
  { pattern: /NATS/i, type: 'queue' },
174
101
  { pattern: /Pulsar/i, type: 'queue' },
@@ -180,7 +107,7 @@ const PARTICIPANT_RULES: readonly InferenceRule[] = [
180
107
  { pattern: /EventHub/i, type: 'queue' },
181
108
  { pattern: /Channel$/i, type: 'queue' },
182
109
 
183
- // ── 6. Actor patterns ──────────────────────────────────
110
+ // ── 4. Actor patterns ──────────────────────────────────
184
111
  // Exact matches first
185
112
  { pattern: /^Admin$/i, type: 'actor' },
186
113
  { pattern: /^User$/i, type: 'actor' },
@@ -199,101 +126,11 @@ const PARTICIPANT_RULES: readonly InferenceRule[] = [
199
126
  { pattern: /^Fan$/i, type: 'actor' },
200
127
  { pattern: /^Purchaser$/i, type: 'actor' },
201
128
  { pattern: /^Reviewer$/i, type: 'actor' },
202
- // Suffix rules (after infrastructure overrides filtered above)
129
+ // Suffix rules
203
130
  { pattern: /User$/i, type: 'actor' },
204
131
  { pattern: /Actor$/i, type: 'actor' },
205
132
  { pattern: /Analyst$/i, type: 'actor' },
206
133
  { pattern: /Staff$/i, type: 'actor' },
207
-
208
- // ── 7. Frontend patterns ────────────────────────────────
209
- { pattern: /App$/i, type: 'frontend' },
210
- { pattern: /Application/i, type: 'frontend' },
211
- { pattern: /Mobile/i, type: 'frontend' },
212
- { pattern: /iOS/i, type: 'frontend' },
213
- { pattern: /Android/i, type: 'frontend' },
214
- { pattern: /Web/i, type: 'frontend' },
215
- { pattern: /Browser/i, type: 'frontend' },
216
- { pattern: /Frontend/i, type: 'frontend' },
217
- { pattern: /UI$/i, type: 'frontend' },
218
- { pattern: /Dashboard/i, type: 'frontend' },
219
- { pattern: /CLI$/i, type: 'frontend' },
220
- { pattern: /Terminal/i, type: 'frontend' },
221
- // Frameworks & patterns
222
- { pattern: /React/i, type: 'frontend' },
223
- { pattern: /^Vue$/i, type: 'frontend' },
224
- { pattern: /Angular/i, type: 'frontend' },
225
- { pattern: /Svelte/i, type: 'frontend' },
226
- { pattern: /NextJS/i, type: 'frontend' },
227
- { pattern: /Nuxt/i, type: 'frontend' },
228
- { pattern: /Remix/i, type: 'frontend' },
229
- { pattern: /Electron/i, type: 'frontend' },
230
- { pattern: /Tauri/i, type: 'frontend' },
231
- { pattern: /Widget$/i, type: 'frontend' },
232
- { pattern: /Portal/i, type: 'frontend' },
233
- { pattern: /Console$/i, type: 'frontend' },
234
- { pattern: /^SPA$/i, type: 'frontend' },
235
- { pattern: /^PWA$/i, type: 'frontend' },
236
-
237
- // ── 8. Service patterns ─────────────────────────────────
238
- { pattern: /Service/i, type: 'service' },
239
- { pattern: /Svc$/i, type: 'service' },
240
- { pattern: /API$/i, type: 'service' },
241
- { pattern: /Lambda/i, type: 'service' },
242
- { pattern: /Function$/i, type: 'service' },
243
- { pattern: /Fn$/i, type: 'service' },
244
- { pattern: /Job$/i, type: 'service' },
245
- { pattern: /Cron/i, type: 'service' },
246
- { pattern: /Microservice/i, type: 'service' },
247
- // Auth
248
- { pattern: /^Auth$/i, type: 'service' },
249
- { pattern: /^AuthN$/i, type: 'service' },
250
- { pattern: /^AuthZ$/i, type: 'service' },
251
- { pattern: /^SSO$/i, type: 'service' },
252
- { pattern: /OAuth/i, type: 'service' },
253
- { pattern: /^OIDC$/i, type: 'service' },
254
- // SaaS
255
- { pattern: /Stripe/i, type: 'service' },
256
- { pattern: /Twilio/i, type: 'service' },
257
- { pattern: /SendGrid/i, type: 'service' },
258
- { pattern: /Mailgun/i, type: 'service' },
259
- // Cloud/infra
260
- { pattern: /^S3$/i, type: 'service' },
261
- { pattern: /^Blob$/i, type: 'service' },
262
- { pattern: /Vercel/i, type: 'service' },
263
- { pattern: /Netlify/i, type: 'service' },
264
- { pattern: /Heroku/i, type: 'service' },
265
- { pattern: /Docker/i, type: 'service' },
266
- { pattern: /Kubernetes/i, type: 'service' },
267
- { pattern: /K8s/i, type: 'service' },
268
- { pattern: /Terraform/i, type: 'service' },
269
- // Security
270
- { pattern: /Vault/i, type: 'service' },
271
- { pattern: /^HSM$/i, type: 'service' },
272
- { pattern: /KMS/i, type: 'service' },
273
- { pattern: /^IAM$/i, type: 'service' },
274
- // AI/ML
275
- { pattern: /^LLM$/i, type: 'service' },
276
- { pattern: /GPT/i, type: 'service' },
277
- { pattern: /^Claude$/i, type: 'service' },
278
- { pattern: /Embedding/i, type: 'service' },
279
- { pattern: /Inference/i, type: 'service' },
280
- // Suffixes & patterns
281
- { pattern: /Pipeline$/i, type: 'service' },
282
- { pattern: /Registry/i, type: 'service' },
283
- { pattern: /Engine$/i, type: 'service' },
284
- { pattern: /Daemon/i, type: 'service' },
285
-
286
- // ── 9. External patterns ────────────────────────────────
287
- { pattern: /External/i, type: 'external' },
288
- { pattern: /Ext$/i, type: 'external' },
289
- { pattern: /ThirdParty/i, type: 'external' },
290
- { pattern: /3P$/i, type: 'external' },
291
- { pattern: /Vendor/i, type: 'external' },
292
- // Named products & patterns
293
- { pattern: /Callback/i, type: 'external' },
294
- { pattern: /^AWS$/i, type: 'external' },
295
- { pattern: /^GCP$/i, type: 'external' },
296
- { pattern: /Azure/i, type: 'external' },
297
134
  ];
298
135
 
299
136
  /**
@@ -49,7 +49,6 @@ const PARTICIPANT_BOX_HEIGHT = 50;
49
49
  const TOP_MARGIN = 20;
50
50
  const TITLE_HEIGHT = 30;
51
51
  const PARTICIPANT_Y_OFFSET = 10;
52
- const SERVICE_BORDER_RADIUS = 10;
53
52
  const MESSAGE_START_OFFSET = 30;
54
53
  const LIFELINE_TAIL = 30;
55
54
  const ARROWHEAD_SIZE = 8;
@@ -180,25 +179,6 @@ function renderRectParticipant(
180
179
  .attr('stroke-width', SW);
181
180
  }
182
181
 
183
- function renderServiceParticipant(
184
- g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
185
- palette: PaletteColors,
186
- isDark: boolean,
187
- color?: string,
188
- solid?: boolean
189
- ): void {
190
- g.append('rect')
191
- .attr('x', -W / 2)
192
- .attr('y', 0)
193
- .attr('width', W)
194
- .attr('height', H)
195
- .attr('rx', SERVICE_BORDER_RADIUS)
196
- .attr('ry', SERVICE_BORDER_RADIUS)
197
- .attr('fill', fill(palette, isDark, color, solid))
198
- .attr('stroke', stroke(palette, color))
199
- .attr('stroke-width', SW);
200
- }
201
-
202
182
  function renderActorParticipant(
203
183
  g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
204
184
  palette: PaletteColors,
@@ -443,99 +423,6 @@ function renderCacheParticipant(
443
423
  .attr('stroke-dasharray', dash);
444
424
  }
445
425
 
446
- function renderNetworkingParticipant(
447
- g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
448
- palette: PaletteColors,
449
- isDark: boolean,
450
- color?: string,
451
- solid?: boolean
452
- ): void {
453
- // Hexagon fitting within W x H
454
- const inset = 16;
455
- const points = [
456
- `${-W / 2 + inset},0`,
457
- `${W / 2 - inset},0`,
458
- `${W / 2},${H / 2}`,
459
- `${W / 2 - inset},${H}`,
460
- `${-W / 2 + inset},${H}`,
461
- `${-W / 2},${H / 2}`,
462
- ].join(' ');
463
- g.append('polygon')
464
- .attr('points', points)
465
- .attr('fill', fill(palette, isDark, color, solid))
466
- .attr('stroke', stroke(palette, color))
467
- .attr('stroke-width', SW);
468
- }
469
-
470
- function renderFrontendParticipant(
471
- g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
472
- palette: PaletteColors,
473
- isDark: boolean,
474
- color?: string,
475
- solid?: boolean
476
- ): void {
477
- // Monitor shape fitting within W x H
478
- const screenH = H - 10;
479
- const s = stroke(palette, color);
480
- g.append('rect')
481
- .attr('x', -W / 2)
482
- .attr('y', 0)
483
- .attr('width', W)
484
- .attr('height', screenH)
485
- .attr('rx', 3)
486
- .attr('ry', 3)
487
- .attr('fill', fill(palette, isDark, color, solid))
488
- .attr('stroke', s)
489
- .attr('stroke-width', SW);
490
- // Stand
491
- g.append('line')
492
- .attr('x1', 0)
493
- .attr('y1', screenH)
494
- .attr('x2', 0)
495
- .attr('y2', H - 2)
496
- .attr('stroke', s)
497
- .attr('stroke-width', SW);
498
- // Base
499
- g.append('line')
500
- .attr('x1', -14)
501
- .attr('y1', H - 2)
502
- .attr('x2', 14)
503
- .attr('y2', H - 2)
504
- .attr('stroke', s)
505
- .attr('stroke-width', SW);
506
- }
507
-
508
- function renderExternalParticipant(
509
- g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
510
- palette: PaletteColors,
511
- isDark: boolean,
512
- color?: string,
513
- solid?: boolean
514
- ): void {
515
- // Dashed border rectangle
516
- g.append('rect')
517
- .attr('x', -W / 2)
518
- .attr('y', 0)
519
- .attr('width', W)
520
- .attr('height', H)
521
- .attr('rx', 2)
522
- .attr('ry', 2)
523
- .attr('fill', fill(palette, isDark, color, solid))
524
- .attr('stroke', stroke(palette, color))
525
- .attr('stroke-width', SW)
526
- .attr('stroke-dasharray', '6 3');
527
- }
528
-
529
- function renderGatewayParticipant(
530
- g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
531
- palette: PaletteColors,
532
- isDark: boolean,
533
- color?: string,
534
- _solid?: boolean
535
- ): void {
536
- renderRectParticipant(g, palette, isDark, color);
537
- }
538
-
539
426
  // ============================================================
540
427
  // Collapsible Section Support
541
428
  // ============================================================
@@ -1528,7 +1415,7 @@ export function renderSequenceDiagram(
1528
1415
  // Ensure contentBottomY accounts for all note extents
1529
1416
  const lastStep = renderSteps[renderSteps.length - 1];
1530
1417
  const lastIsSelfCall =
1531
- lastStep && lastStep.type === 'call' && lastStep.from === lastStep.to;
1418
+ lastStep?.type === 'call' && lastStep.from === lastStep.to;
1532
1419
  const lastStepTrailing = lastIsSelfCall ? SELF_CALL_HEIGHT + 25 : stepSpacing;
1533
1420
  let contentBottomY =
1534
1421
  renderSteps.length > 0
@@ -1968,6 +1855,13 @@ export function renderSequenceDiagram(
1968
1855
  // FRAME_PADDING_TOP declared earlier (near BLOCK_HEADER_SPACE)
1969
1856
  const FRAME_PADDING_BOTTOM = 15;
1970
1857
  const FRAME_LABEL_HEIGHT = 18;
1858
+ // Self-loop projects ACTIVATION_WIDTH/2 + SELF_CALL_WIDTH (=35) past the
1859
+ // lifeline; FRAME_PADDING_X (=30) leaves no breathing room. When a block
1860
+ // contains a self-arrow, extend the frame on the loop's side so the loop
1861
+ // sits comfortably inside.
1862
+ const SELF_ARROW_PROJECTION = ACTIVATION_WIDTH / 2 + SELF_CALL_WIDTH;
1863
+ const SELF_ARROW_FRAME_PAD = 10;
1864
+ const frameRightmostX = Math.max(...Array.from(participantX.values()));
1971
1865
 
1972
1866
  // Collect message indices from an element subtree
1973
1867
  const collectMsgIndices = (els: SequenceElement[]): number[] => {
@@ -2063,14 +1957,46 @@ export function renderSequenceDiagram(
2063
1957
  }
2064
1958
  }
2065
1959
 
2066
- const frameX = minPX - FRAME_PADDING_X;
1960
+ // Self-arrow geometry: extend frame on the loop's side so loops sit
1961
+ // comfortably inside, and extend vertically if the block's last step
1962
+ // is a self-call (whose loop drops SELF_CALL_HEIGHT below stepY).
1963
+ let extraLeft = 0;
1964
+ let extraRight = 0;
1965
+ let maxStepIsSelfCall = false;
1966
+ for (const mi of allIndices) {
1967
+ const m = messages[mi];
1968
+ if (m.from === m.to) {
1969
+ const px = participantX.get(m.from);
1970
+ if (px !== undefined) {
1971
+ const flipLeft = px === frameRightmostX;
1972
+ if (flipLeft) {
1973
+ const loopMin = px - SELF_ARROW_PROJECTION;
1974
+ const need =
1975
+ minPX - FRAME_PADDING_X - loopMin + SELF_ARROW_FRAME_PAD;
1976
+ if (need > 0) extraLeft = Math.max(extraLeft, need);
1977
+ } else {
1978
+ const loopMax = px + SELF_ARROW_PROJECTION;
1979
+ const need =
1980
+ loopMax - (maxPX + FRAME_PADDING_X) + SELF_ARROW_FRAME_PAD;
1981
+ if (need > 0) extraRight = Math.max(extraRight, need);
1982
+ }
1983
+ }
1984
+ if (msgToLastStep.get(mi) === maxStep) {
1985
+ maxStepIsSelfCall = true;
1986
+ }
1987
+ }
1988
+ }
1989
+
1990
+ const frameX = minPX - FRAME_PADDING_X - extraLeft;
2067
1991
  const frameY = stepY(minStep) - FRAME_PADDING_TOP;
2068
- const frameW = maxPX - minPX + FRAME_PADDING_X * 2;
1992
+ const frameW =
1993
+ maxPX - minPX + FRAME_PADDING_X * 2 + extraLeft + extraRight;
2069
1994
  const frameH =
2070
1995
  stepY(maxStep) -
2071
1996
  stepY(minStep) +
2072
1997
  FRAME_PADDING_TOP +
2073
- FRAME_PADDING_BOTTOM;
1998
+ FRAME_PADDING_BOTTOM +
1999
+ (maxStepIsSelfCall ? SELF_CALL_HEIGHT : 0);
2074
2000
 
2075
2001
  // Frame border
2076
2002
  svg
@@ -2093,7 +2019,7 @@ export function renderSequenceDiagram(
2093
2019
  x: frameX + 6,
2094
2020
  y: frameY + FRAME_LABEL_HEIGHT - 4,
2095
2021
  text: `${el.type} ${el.label}`,
2096
- bold: true,
2022
+ bold: false,
2097
2023
  italic: false,
2098
2024
  blockLine: el.lineNumber,
2099
2025
  });
@@ -2121,7 +2047,7 @@ export function renderSequenceDiagram(
2121
2047
  y: dividerY + 14,
2122
2048
  text: `else if ${branchData.label}`,
2123
2049
  bold: false,
2124
- italic: true,
2050
+ italic: false,
2125
2051
  blockLine: branchData.lineNumber,
2126
2052
  });
2127
2053
  }
@@ -2150,7 +2076,7 @@ export function renderSequenceDiagram(
2150
2076
  y: dividerY + 14,
2151
2077
  text: 'else',
2152
2078
  bold: false,
2153
- italic: true,
2079
+ italic: false,
2154
2080
  blockLine: el.elseLineNumber,
2155
2081
  });
2156
2082
  }
@@ -2292,7 +2218,7 @@ export function renderSequenceDiagram(
2292
2218
 
2293
2219
  // Render section dividers
2294
2220
  const leftmostX = Math.min(...Array.from(participantX.values()));
2295
- const rightmostX = Math.max(...Array.from(participantX.values()));
2221
+ const rightmostX = frameRightmostX;
2296
2222
  const sectionLineX1 = leftmostX - PARTICIPANT_BOX_WIDTH / 2 - 10;
2297
2223
  const sectionLineX2 = rightmostX + PARTICIPANT_BOX_WIDTH / 2 + 10;
2298
2224
 
@@ -2853,27 +2779,12 @@ function renderParticipant(
2853
2779
  case 'database':
2854
2780
  renderDatabaseParticipant(g, palette, isDark, color, solid);
2855
2781
  break;
2856
- case 'service':
2857
- renderServiceParticipant(g, palette, isDark, color, solid);
2858
- break;
2859
2782
  case 'queue':
2860
2783
  renderQueueParticipant(g, palette, isDark, color, solid);
2861
2784
  break;
2862
2785
  case 'cache':
2863
2786
  renderCacheParticipant(g, palette, isDark, color, solid);
2864
2787
  break;
2865
- case 'networking':
2866
- renderNetworkingParticipant(g, palette, isDark, color, solid);
2867
- break;
2868
- case 'frontend':
2869
- renderFrontendParticipant(g, palette, isDark, color, solid);
2870
- break;
2871
- case 'external':
2872
- renderExternalParticipant(g, palette, isDark, color, solid);
2873
- break;
2874
- case 'gateway':
2875
- renderGatewayParticipant(g, palette, isDark, color, solid);
2876
- break;
2877
2788
  default:
2878
2789
  renderRectParticipant(g, palette, isDark, color, solid);
2879
2790
  break;
@@ -162,7 +162,7 @@ export function parseSitemap(
162
162
  result.diagnostics.push(makeDgmoError(line, message, 'warning'));
163
163
  };
164
164
 
165
- if (!content || !content.trim()) {
165
+ if (!content?.trim()) {
166
166
  return fail(0, 'No content provided');
167
167
  }
168
168