@brainwav/diagram 1.0.3 → 1.0.5

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 (3) hide show
  1. package/README.md +57 -3
  2. package/package.json +1 -1
  3. package/src/diagram.js +681 -4
package/README.md CHANGED
@@ -11,6 +11,7 @@ Generate codebase architecture diagrams from source files. No AI required.
11
11
  - [First-run checklist](#first-run-checklist)
12
12
  - [Commands](#commands)
13
13
  - [Diagram types](#diagram-types)
14
+ - [AI-focused diagram outputs](#ai-focused-diagram-outputs)
14
15
  - [Output formats](#output-formats)
15
16
  - [Video and animation prerequisites](#video-and-animation-prerequisites)
16
17
  - [Architecture Testing](#architecture-testing)
@@ -119,7 +120,7 @@ diagram generate . --open
119
120
 
120
121
  Options:
121
122
 
122
- - `-t, --type <type>` `architecture|sequence|dependency|class|flow` (default: `architecture`)
123
+ - `-t, --type <type>` `architecture|sequence|dependency|class|flow|database|user|events|auth|security` (default: `architecture`)
123
124
  - `-f, --focus <module>` focus on one module or directory
124
125
  - `-o, --output <file>` write `.mmd`, `.svg`, or `.png`
125
126
  - `-m, --max-files <n>` max files to analyze
@@ -139,6 +140,23 @@ Options:
139
140
 
140
141
  - `-o, --output-dir <dir>` output directory (default: `./diagrams`)
141
142
 
143
+ ### `diagram manifest [path]`
144
+
145
+ Summarize the generated `.diagram/manifest.json` artifact.
146
+
147
+ ```bash
148
+ diagram manifest .
149
+ diagram manifest . --manifest-dir .diagram --output .diagram/manifest-summary.json
150
+ diagram manifest . --manifest-dir .diagram --require-types architecture,security --fail-on-placeholder
151
+ ```
152
+
153
+ Options:
154
+
155
+ - `-d, --manifest-dir <dir>` directory containing `manifest.json` (default: `.diagram`)
156
+ - `-o, --output <file>` write summary JSON to file
157
+ - `--require-types <list>` require specific diagram types, comma-separated
158
+ - `--fail-on-placeholder` fail if any diagram entry is a placeholder
159
+
142
160
  ### `diagram video [path]`
143
161
 
144
162
  Generate an animated video (`.mp4`, `.webm`, `.mov`) from a Mermaid diagram.
@@ -151,7 +169,7 @@ diagram video . --duration 8 --fps 60 --width 1920 --height 1080
151
169
 
152
170
  Options:
153
171
 
154
- - `-t, --type <type>` `architecture|sequence|dependency|class|flow` (default: `architecture`)
172
+ - `-t, --type <type>` `architecture|sequence|dependency|class|flow|database|user|events|auth|security` (default: `architecture`)
155
173
  - `-o, --output <file>` output file (default: `diagram.mp4`)
156
174
  - `-d, --duration <sec>` video duration in seconds (default: `5`)
157
175
  - `-f, --fps <n>` frames per second (default: `30`)
@@ -172,7 +190,7 @@ diagram animate . --theme forest
172
190
 
173
191
  Options:
174
192
 
175
- - `-t, --type <type>` `architecture|sequence|dependency|class|flow` (default: `architecture`)
193
+ - `-t, --type <type>` `architecture|sequence|dependency|class|flow|database|user|events|auth|security` (default: `architecture`)
176
194
  - `-o, --output <file>` output file (default: `diagram-animated.svg`)
177
195
  - `--theme <theme>` `default|dark|forest|neutral` (default: `dark`)
178
196
  - `-m, --max-files <n>` max files to analyze (default: `100`)
@@ -186,6 +204,41 @@ Options:
186
204
  | `dependency` | Internal and external imports | Dependency review |
187
205
  | `class` | Class-oriented relationships | OOP-heavy codebases |
188
206
  | `flow` | Process/data flow | Control-flow mapping |
207
+ | `database` | Database operations and condition paths | Conditional persistence flows |
208
+ | `user` | User-facing entrypoints and handlers | Interaction flow mapping |
209
+ | `events` | Event streams and async channels | Event-driven architecture |
210
+ | `auth` | Authentication and authorization checks | Credential/identity flow |
211
+ | `security` | Security boundaries and trust paths | Threat/risk analysis |
212
+
213
+ ## AI-focused diagram outputs
214
+
215
+ For agent workflows, the Mermaid output is especially useful because it is
216
+ compact, textual, and structured. Feeding `.mmd` into an AI at startup lets it
217
+ understand architecture faster than reading all source files.
218
+
219
+ The generated types cover these high-value areas for automated reasoning:
220
+
221
+ - **Database Operations** — conditional record paths (for example "record exists?"
222
+ / "not found" branches), storage and mutation decisions.
223
+ - **User Actions and Interactions** — user entrypoints and downstream handler
224
+ chains.
225
+ - **Events and Channels** — internal publishers, workers, listeners, and trigger
226
+ paths.
227
+ - **Authentication Flows** — step-by-step identity and credential checks.
228
+ - **Security and Data Flows** — trust boundaries, sensitive components, and
229
+ integrations to support security review and compliance context.
230
+
231
+ When reviewing PRs, run:
232
+
233
+ ```bash
234
+ diagram all . --output-dir .diagram
235
+ ```
236
+
237
+ so `.diagram/` includes the new AI-oriented variants beside the classic ones.
238
+
239
+ The command also writes `.diagram/manifest.json` summarizing what diagrams were
240
+ produced and whether any outputs are placeholder/no-data (helpful for CI and
241
+ agent bootstrap checks).
189
242
 
190
243
  ## Output formats
191
244
 
@@ -194,6 +247,7 @@ Options:
194
247
  - `.svg`/`.png` rendered images (requires Mermaid CLI)
195
248
  - `.mp4`/`.webm`/`.mov` video export (requires Playwright + ffmpeg)
196
249
  - Animated `.svg` export (requires Playwright)
250
+ - `.diagram/manifest.json` machine-readable artifact index
197
251
 
198
252
  Install Mermaid CLI for image export:
199
253
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainwav/diagram",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Generate architecture diagrams from codebases",
5
5
  "main": "src/diagram.js",
6
6
  "bin": {
package/src/diagram.js CHANGED
@@ -226,6 +226,231 @@ function getExternalPackageName(importPath) {
226
226
  return importPath.split('/')[0] || null;
227
227
  }
228
228
 
229
+ const ROLE_PATTERNS = {
230
+ user: [
231
+ 'route', 'routes', 'controller', 'controllers', 'handler', 'handlers',
232
+ 'api', 'middleware', 'page', 'pages', 'ui', 'frontend', 'web', 'client', 'request'
233
+ ],
234
+ auth: [
235
+ 'auth', 'authentication', 'authorization', 'session', 'signin', 'login',
236
+ 'signup', 'token', 'jwt', 'oauth', 'sso', 'passport', 'identity', 'acl',
237
+ 'guard', 'permission', 'password', 'mfa', 'security'
238
+ ],
239
+ database: [
240
+ 'db', 'database', 'data', 'datastore', 'repository', 'repo', 'model',
241
+ 'schema', 'migration', 'query', 'querybuilder', 'prisma', 'typeorm',
242
+ 'sequelize', 'mongoose', 'knex', 'drizzle', 'redis', 'postgres', 'mysql',
243
+ 'sqlite', 'mongo', 'dynamodb', 'd1'
244
+ ],
245
+ events: [
246
+ 'event', 'events', 'queue', 'worker', 'cron', 'scheduler', 'webhook',
247
+ 'pubsub', 'bus', 'publish', 'subscriber', 'consumer', 'producer',
248
+ 'listener', 'trigger'
249
+ ],
250
+ integrations: [
251
+ 'integration', 'webhook', 'gateway', 'stripe', 'pay', 'sendgrid', 'twilio',
252
+ 'sentry', 'github', 'slack', 'analytics', 'mail', 'smtp', 'storage'
253
+ ],
254
+ security: [
255
+ 'security', 'threat', 'attack', 'rate', 'encrypt', 'decrypt', 'signature',
256
+ 'hash', 'verify', 'csrf', 'xss', 'audit', 'compliance', 'policy', 'vault',
257
+ 'kms', 'secret', 'key'
258
+ ],
259
+ };
260
+
261
+ const SUPPORTED_DIAGRAM_TYPES = Object.freeze([
262
+ 'architecture',
263
+ 'sequence',
264
+ 'dependency',
265
+ 'class',
266
+ 'flow',
267
+ 'database',
268
+ 'user',
269
+ 'events',
270
+ 'auth',
271
+ 'security',
272
+ ]);
273
+
274
+ function textHasToken(text, token) {
275
+ const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
276
+ const re = new RegExp(`(^|[\\/._-])${escaped}([\\/._-]|$)`, 'i');
277
+ return re.test(text);
278
+ }
279
+
280
+ function collectExternalImports(importEntries) {
281
+ const packages = new Set();
282
+ if (!Array.isArray(importEntries)) return [];
283
+
284
+ for (const entry of importEntries) {
285
+ const importPath = getImportPath(entry);
286
+ if (!importPath || importPath.startsWith('.')) {
287
+ continue;
288
+ }
289
+ const externalPackage = getExternalPackageName(importPath);
290
+ if (externalPackage) {
291
+ packages.add(externalPackage);
292
+ }
293
+ }
294
+
295
+ return [...packages];
296
+ }
297
+
298
+ function inferRoleTags(filePath, originalName, fileContent, importEntries, type) {
299
+ const content = (fileContent || '').toLowerCase();
300
+ const pathText = normalizePath(filePath || '').toLowerCase();
301
+ const nameText = (originalName || '').toLowerCase();
302
+ const externalImports = collectExternalImports(importEntries).join(' ').toLowerCase();
303
+ const combined = `${pathText} ${nameText} ${content} ${externalImports}`;
304
+
305
+ const tags = new Set();
306
+
307
+ for (const [tag, tokens] of Object.entries(ROLE_PATTERNS)) {
308
+ for (const token of tokens) {
309
+ if (textHasToken(combined, token)) {
310
+ tags.add(tag);
311
+ break;
312
+ }
313
+ }
314
+ }
315
+
316
+ if (type === 'service') {
317
+ tags.add('service');
318
+ }
319
+
320
+ if (tags.size === 0) {
321
+ tags.add('general');
322
+ }
323
+
324
+ return [...tags];
325
+ }
326
+
327
+ function hasRole(component, role) {
328
+ return (Array.isArray(component.roleTags) && component.roleTags.includes(role));
329
+ }
330
+
331
+ function componentsByRole(components, role) {
332
+ if (!Array.isArray(components)) return [];
333
+ return components.filter((component) => hasRole(component, role));
334
+ }
335
+
336
+ function getExternalPackageList(importEntries) {
337
+ const packages = collectExternalImports(importEntries);
338
+ if (!packages.length) return [];
339
+ return packages.map((pkg) => ({
340
+ name: pkg,
341
+ label: pkg,
342
+ }));
343
+ }
344
+
345
+ function mapSafeNames(components) {
346
+ const map = new Map();
347
+ const used = new Set();
348
+
349
+ for (const component of components) {
350
+ const rawName = sanitize(component.name || component.originalName || 'node');
351
+ if (!used.has(rawName)) {
352
+ map.set(component, rawName);
353
+ used.add(rawName);
354
+ continue;
355
+ }
356
+
357
+ let i = 1;
358
+ let candidate = `${rawName}_${i}`;
359
+ while (used.has(candidate)) {
360
+ i += 1;
361
+ candidate = `${rawName}_${i}`;
362
+ }
363
+ map.set(component, candidate);
364
+ used.add(candidate);
365
+ }
366
+
367
+ return map;
368
+ }
369
+
370
+ function byNameIndex(components) {
371
+ const map = new Map();
372
+ if (!Array.isArray(components)) return map;
373
+ for (const component of components) {
374
+ if (component && component.name) {
375
+ map.set(component.name, component);
376
+ }
377
+ }
378
+ return map;
379
+ }
380
+
381
+ function resolveDependencyComponent(component, componentsByName, name) {
382
+ if (!component || !name || !componentsByName) return null;
383
+ return componentsByName.get(name) || null;
384
+ }
385
+
386
+ function collectConnectedComponents(components, seedComponents, maxDepth = 2, maxNodes = 35) {
387
+ if (!Array.isArray(components)) return [];
388
+ if (!Array.isArray(seedComponents) || seedComponents.length === 0) return [];
389
+
390
+ const byName = byNameIndex(components);
391
+ const selected = new Map();
392
+ const queue = [];
393
+
394
+ for (const seed of seedComponents) {
395
+ if (seed && seed.name && !selected.has(seed.name)) {
396
+ selected.set(seed.name, seed);
397
+ queue.push(seed);
398
+ }
399
+ }
400
+
401
+ let depth = 0;
402
+ const visited = new Set();
403
+ while (queue.length > 0 && depth < maxDepth) {
404
+ const levelSize = queue.length;
405
+ for (let i = 0; i < levelSize; i++) {
406
+ const current = queue.shift();
407
+ if (!current || typeof current.name !== 'string') continue;
408
+ const depthKey = `${current.name}:${depth}`;
409
+ if (visited.has(depthKey)) continue;
410
+ visited.add(depthKey);
411
+
412
+ const next = [];
413
+ for (const depName of current.dependencies || []) {
414
+ const dependency = byName.get(depName);
415
+ if (dependency && !selected.has(depName)) {
416
+ selected.set(depName, dependency);
417
+ next.push(dependency);
418
+ }
419
+ }
420
+
421
+ for (const candidate of components) {
422
+ if (selected.has(candidate.name)) continue;
423
+ const reverseDependencies = Array.isArray(candidate.dependencies) ? candidate.dependencies : [];
424
+ if (reverseDependencies.includes(current.name)) {
425
+ selected.set(candidate.name, candidate);
426
+ next.push(candidate);
427
+ }
428
+ }
429
+
430
+ for (const n of next) {
431
+ if (selected.size >= maxNodes) break;
432
+ queue.push(n);
433
+ }
434
+ if (selected.size >= maxNodes) break;
435
+ }
436
+ depth += 1;
437
+ }
438
+
439
+ return [...selected.values()];
440
+ }
441
+
442
+ function inferDbIntent(component) {
443
+ const source = `${component.filePath || ''} ${component.originalName || ''} ${component.name || ''}`.toLowerCase();
444
+ const hasLookup = /(read|find|query|select|get|lookup|exists|fetch)/.test(source);
445
+ const hasWrite = /(create|insert|update|upsert|save|delete|remove|write|transaction)/.test(source);
446
+ return { hasLookup, hasWrite };
447
+ }
448
+
449
+ function classifyAsGeneral(component) {
450
+ if (!component || !Array.isArray(component.roleTags)) return false;
451
+ return component.roleTags.includes('general') && component.roleTags.length === 1;
452
+ }
453
+
229
454
  // Analysis
230
455
  async function analyze(rootPath, options) {
231
456
  // Validate maxFiles with strict parsing
@@ -306,12 +531,16 @@ async function analyze(rootPath, options) {
306
531
  }
307
532
  seenNames.add(uniqueName);
308
533
 
534
+ const imports = extractImportsWithPositions(content, lang);
535
+ const type = inferType(filePath, content);
536
+
309
537
  components.push({
310
538
  name: uniqueName,
311
539
  originalName: baseName,
312
540
  filePath: rel,
313
- type: inferType(filePath, content),
314
- imports: extractImportsWithPositions(content, lang),
541
+ type,
542
+ imports,
543
+ roleTags: inferRoleTags(rel, baseName, content, imports, type),
315
544
  directory: dir,
316
545
  });
317
546
  } catch (e) {
@@ -563,6 +792,282 @@ function generateFlow(data) {
563
792
  return lines.join('\n');
564
793
  }
565
794
 
795
+ function generateDatabase(data) {
796
+ if (!data || !Array.isArray(data.components)) {
797
+ return 'flowchart TD\n Note["No data available"]';
798
+ }
799
+
800
+ const lines = ['flowchart TD'];
801
+ const seeds = componentsByRole(data.components, 'database');
802
+ if (seeds.length === 0) {
803
+ lines.push(' Note["No database-focused components found"]');
804
+ return lines.join('\n');
805
+ }
806
+
807
+ const connected = collectConnectedComponents(data.components, seeds, 2, 28);
808
+ const byName = byNameIndex(connected);
809
+ const safeNames = mapSafeNames(connected);
810
+
811
+ lines.push(' UserRequest["User request"]');
812
+ lines.push(' Decision{Record exists?}');
813
+
814
+ const addedEdges = new Set();
815
+ for (const comp of connected) {
816
+ if (!seeds.includes(comp)) continue;
817
+ const safe = safeNames.get(comp);
818
+ if (!safe) continue;
819
+ lines.push(` ${safe}["${escapeMermaid(comp.originalName)}"]`);
820
+ lines.push(` UserRequest --> ${safe}`);
821
+
822
+ const intent = inferDbIntent(comp);
823
+ if (intent.hasLookup) {
824
+ const lookup = `${safe}_lookup`;
825
+ const create = `${safe}_create`;
826
+ const update = `${safe}_update`;
827
+ lines.push(` ${safe} --> ${lookup}["lookup query"]`);
828
+ lines.push(` ${lookup} --> Decision`);
829
+ lines.push(` Decision -->|found| ${update}["update or modify"]`);
830
+ lines.push(` Decision -->|not found| ${create}["insert/create"]`);
831
+ lines.push(` ${update} --> ${safe}_result["result"]`);
832
+ lines.push(` ${create} --> ${safe}_result["result"]`);
833
+ } else if (intent.hasWrite) {
834
+ const write = `${safe}_write`;
835
+ lines.push(` ${safe} --> ${write}["write/update"]`);
836
+ lines.push(` ${write} --> ${safe}_result["result"]`);
837
+ } else {
838
+ const result = `${safe}_result`;
839
+ lines.push(` ${safe} --> ${result}["result"]`);
840
+ }
841
+
842
+ for (const depName of comp.dependencies || []) {
843
+ const dep = byName.get(depName);
844
+ if (!dep || !safeNames.has(dep)) continue;
845
+ const edge = `${safe}->${safeNames.get(dep)}`;
846
+ if (!addedEdges.has(edge)) {
847
+ addedEdges.add(edge);
848
+ lines.push(` ${safe} --> ${safeNames.get(dep)}`);
849
+ }
850
+ }
851
+ }
852
+
853
+ lines.push(' classDef dbNode fill:#0ea5e9,color:#fff');
854
+ lines.push(' classDef decisionNode fill:#0284c7,color:#fff');
855
+ return lines.join('\n');
856
+ }
857
+
858
+ function generateUserInteractions(data) {
859
+ if (!data || !Array.isArray(data.components)) {
860
+ return 'flowchart LR\n Note["No data available"]';
861
+ }
862
+
863
+ const lines = ['flowchart LR'];
864
+ const seeds = componentsByRole(data.components, 'user');
865
+ if (seeds.length === 0) {
866
+ lines.push(' Note["No user-facing components found"]');
867
+ return lines.join('\n');
868
+ }
869
+
870
+ const connected = collectConnectedComponents(data.components, seeds, 1, 30);
871
+ const byName = byNameIndex(connected);
872
+ const safeNames = mapSafeNames(connected);
873
+ const edges = new Set();
874
+
875
+ lines.push(' User(("User"))');
876
+ for (const seed of seeds) {
877
+ const safe = safeNames.get(seed);
878
+ if (!safe) continue;
879
+ lines.push(` ${safe}["${escapeMermaid(seed.originalName)}"]`);
880
+ lines.push(` User --> ${safe}`);
881
+ }
882
+
883
+ for (const comp of connected) {
884
+ const from = safeNames.get(comp);
885
+ if (!from) continue;
886
+ for (const depName of comp.dependencies || []) {
887
+ const dep = byName.get(depName);
888
+ if (!dep) continue;
889
+ const to = safeNames.get(dep);
890
+ if (!to) continue;
891
+ const key = `${from}->${to}`;
892
+ if (!edges.has(key)) {
893
+ edges.add(key);
894
+ lines.push(` ${from} --> ${to}`);
895
+ }
896
+ }
897
+ }
898
+
899
+ lines.push(' classDef userNode fill:#16a34a,color:#fff');
900
+ return lines.join('\n');
901
+ }
902
+
903
+ function generateEvents(data) {
904
+ if (!data || !Array.isArray(data.components)) {
905
+ return 'flowchart TD\n Note["No data available"]';
906
+ }
907
+
908
+ const lines = ['flowchart TD'];
909
+ const seeds = componentsByRole(data.components, 'events');
910
+ if (seeds.length === 0) {
911
+ lines.push(' Note["No event/channels components found"]');
912
+ return lines.join('\n');
913
+ }
914
+
915
+ const connected = collectConnectedComponents(data.components, seeds, 2, 30);
916
+ const byName = byNameIndex(connected);
917
+ const safeNames = mapSafeNames(connected);
918
+ const edges = new Set();
919
+
920
+ lines.push(' subgraph Channels["Event channels / queues"]');
921
+ for (const component of connected) {
922
+ const safe = safeNames.get(component);
923
+ if (!safe) continue;
924
+ const isEventSource = seeds.includes(component);
925
+ if (isEventSource) {
926
+ lines.push(` ${safe}{{"${escapeMermaid(component.originalName)}"}}`);
927
+ } else {
928
+ lines.push(` ${safe}["${escapeMermaid(component.originalName)}"]`);
929
+ }
930
+ }
931
+ lines.push(' end');
932
+
933
+ for (const comp of connected) {
934
+ const from = safeNames.get(comp);
935
+ if (!from) continue;
936
+ for (const depName of comp.dependencies || []) {
937
+ const dep = byName.get(depName);
938
+ if (!dep) continue;
939
+ const to = safeNames.get(dep);
940
+ if (!to) continue;
941
+ const edge = `${from}->${to}`;
942
+ if (!edges.has(edge)) {
943
+ edges.add(edge);
944
+ const label = seeds.includes(comp) ? '|emit|' : '|consume|';
945
+ lines.push(` ${from} -->${label} ${to}`);
946
+ }
947
+ }
948
+ }
949
+
950
+ lines.push(' classDef eventNode fill:#db2777,color:#fff');
951
+ return lines.join('\n');
952
+ }
953
+
954
+ function generateAuth(data) {
955
+ if (!data || !Array.isArray(data.components)) {
956
+ return 'flowchart TD\n Note["No data available"]';
957
+ }
958
+
959
+ const lines = ['flowchart TD'];
960
+ const seeds = componentsByRole(data.components, 'auth');
961
+ if (seeds.length === 0) {
962
+ lines.push(' Note["No authentication components found"]');
963
+ return lines.join('\n');
964
+ }
965
+
966
+ const connected = collectConnectedComponents(data.components, seeds, 2, 24);
967
+ const byName = byNameIndex(connected);
968
+ const safeNames = mapSafeNames(connected);
969
+ const edges = new Set();
970
+
971
+ lines.push(' Request["Authentication request"]');
972
+ lines.push(' Boundary{"Auth Boundary"}');
973
+ lines.push(' Request --> Boundary');
974
+
975
+ for (const seed of seeds) {
976
+ const safe = safeNames.get(seed);
977
+ if (!safe) continue;
978
+ lines.push(` ${safe}["${escapeMermaid(seed.originalName)}"]`);
979
+ const key = `Boundary->${safe}`;
980
+ if (!edges.has(key)) {
981
+ edges.add(key);
982
+ lines.push(` Boundary --> ${safe}`);
983
+ }
984
+ }
985
+
986
+ for (const comp of connected) {
987
+ const from = safeNames.get(comp);
988
+ if (!from) continue;
989
+ for (const depName of comp.dependencies || []) {
990
+ const dep = byName.get(depName);
991
+ if (!dep) continue;
992
+ const to = safeNames.get(dep);
993
+ if (!to) continue;
994
+ const key = `${from}->${to}`;
995
+ if (!edges.has(key)) {
996
+ edges.add(key);
997
+ lines.push(` ${from} --> ${to}`);
998
+ }
999
+ }
1000
+ }
1001
+
1002
+ const providerSet = new Set();
1003
+ for (const seed of seeds) {
1004
+ for (const pkg of collectExternalImports(seed.imports || [])) {
1005
+ providerSet.add(pkg);
1006
+ }
1007
+ }
1008
+ for (const provider of providerSet) {
1009
+ const providerNode = sanitize(provider);
1010
+ lines.push(` ${providerNode}[("${escapeMermaid(provider)}")]`);
1011
+ }
1012
+
1013
+ lines.push(' classDef authNode fill:#7c3aed,color:#fff');
1014
+ return lines.join('\n');
1015
+ }
1016
+
1017
+ function generateSecurity(data) {
1018
+ if (!data || !Array.isArray(data.components)) {
1019
+ return 'flowchart TD\n Note["No data available"]';
1020
+ }
1021
+
1022
+ const lines = ['flowchart TD'];
1023
+ const seeds = [
1024
+ ...componentsByRole(data.components, 'security'),
1025
+ ...componentsByRole(data.components, 'auth'),
1026
+ ...componentsByRole(data.components, 'integrations'),
1027
+ ].filter((value, index, arr) => arr.indexOf(value) === index);
1028
+
1029
+ if (seeds.length === 0) {
1030
+ lines.push(' Note["No security-focused components found"]');
1031
+ return lines.join('\n');
1032
+ }
1033
+
1034
+ const connected = collectConnectedComponents(data.components, seeds, 2, 40);
1035
+ const byName = byNameIndex(connected);
1036
+ const safeNames = mapSafeNames(connected);
1037
+ const edges = new Set();
1038
+
1039
+ lines.push(' Untrusted["Untrusted input"]');
1040
+ for (const seed of seeds) {
1041
+ const safe = safeNames.get(seed);
1042
+ if (!safe) continue;
1043
+ lines.push(` ${safe}["${escapeMermaid(seed.originalName)}"]`);
1044
+ const key = `Untrusted->${safe}`;
1045
+ if (!edges.has(key)) {
1046
+ edges.add(key);
1047
+ lines.push(` Untrusted --> ${safe}`);
1048
+ }
1049
+ }
1050
+
1051
+ for (const comp of connected) {
1052
+ const from = safeNames.get(comp);
1053
+ if (!from) continue;
1054
+ for (const depName of comp.dependencies || []) {
1055
+ const dep = byName.get(depName);
1056
+ if (!dep) continue;
1057
+ const to = safeNames.get(dep);
1058
+ if (!to) continue;
1059
+ const key = `${from}->${to}`;
1060
+ if (!edges.has(key)) {
1061
+ edges.add(key);
1062
+ lines.push(` ${from} --> ${to}`);
1063
+ }
1064
+ }
1065
+ }
1066
+
1067
+ lines.push(' classDef securityNode fill:#dc2626,color:#fff');
1068
+ return lines.join('\n');
1069
+ }
1070
+
566
1071
  function generate(data, type, focus) {
567
1072
  switch (type) {
568
1073
  case 'architecture': return generateArchitecture(data, focus);
@@ -570,12 +1075,79 @@ function generate(data, type, focus) {
570
1075
  case 'dependency': return generateDependency(data, focus);
571
1076
  case 'class': return generateClass(data);
572
1077
  case 'flow': return generateFlow(data);
1078
+ case 'database': return generateDatabase(data);
1079
+ case 'user': return generateUserInteractions(data);
1080
+ case 'events': return generateEvents(data);
1081
+ case 'auth': return generateAuth(data);
1082
+ case 'security': return generateSecurity(data);
573
1083
  default:
574
1084
  console.warn(chalk.yellow(`⚠️ Unknown diagram type "${type}", using architecture`));
575
1085
  return generateArchitecture(data, focus);
576
1086
  }
577
1087
  }
578
1088
 
1089
+ function isPlaceholderDiagram(mermaidCode) {
1090
+ if (!mermaidCode || typeof mermaidCode !== 'string') return true;
1091
+ const compact = mermaidCode.toLowerCase();
1092
+ return compact.includes('note["no data available"]')
1093
+ || compact.includes('note["no components found')
1094
+ || compact.includes('no services detected')
1095
+ || compact.includes('note "no data available"')
1096
+ || compact.includes('note "no classes found"')
1097
+ || compact.includes('note["no database-focused components found"]')
1098
+ || compact.includes('note["no user-facing components found"]')
1099
+ || compact.includes('note["no event/channels components found"]')
1100
+ || compact.includes('note["no authentication components found"]')
1101
+ || compact.includes('note["no security-focused components found"]')
1102
+ || compact.includes('no architecture data');
1103
+ }
1104
+
1105
+ function toManifestEntry(type, filePath, mermaidCode, rootPath) {
1106
+ const lines = typeof mermaidCode === 'string' ? mermaidCode.split('\n') : [];
1107
+ return {
1108
+ type,
1109
+ file: path.basename(filePath),
1110
+ outputPath: rootPath ? path.relative(rootPath, filePath) : filePath,
1111
+ lines: lines.length,
1112
+ bytes: Buffer.byteLength(mermaidCode || '', 'utf8'),
1113
+ isPlaceholder: isPlaceholderDiagram(mermaidCode),
1114
+ };
1115
+ }
1116
+
1117
+ function parseCommaSeparatedList(value) {
1118
+ if (!value || typeof value !== 'string') return [];
1119
+ return value.split(',').map((item) => item.trim()).filter(Boolean);
1120
+ }
1121
+
1122
+ function buildManifestSummary(manifest) {
1123
+ if (!manifest || !Array.isArray(manifest.diagrams)) {
1124
+ return null;
1125
+ }
1126
+
1127
+ const diagrams = manifest.diagrams
1128
+ .map((diagram) => ({
1129
+ ...diagram,
1130
+ isPlaceholder: Boolean(diagram.isPlaceholder),
1131
+ }))
1132
+ .filter((entry) => entry && typeof entry.type === 'string' && entry.file);
1133
+
1134
+ const missing = SUPPORTED_DIAGRAM_TYPES.filter(
1135
+ (type) => !diagrams.some((diagram) => diagram.type === type)
1136
+ );
1137
+ const placeholderTypes = diagrams.filter((diagram) => diagram.isPlaceholder).map((diagram) => diagram.type);
1138
+
1139
+ return {
1140
+ generatedAt: manifest.generatedAt || new Date().toISOString(),
1141
+ rootPath: manifest.rootPath,
1142
+ diagramDir: manifest.diagramDir,
1143
+ totalDiagrams: diagrams.length,
1144
+ placeholders: placeholderTypes.length,
1145
+ placeholderTypes,
1146
+ missingTypes: missing,
1147
+ diagrams,
1148
+ };
1149
+ }
1150
+
579
1151
  // URL shortening for large diagrams
580
1152
  function createMermaidUrl(mermaidCode) {
581
1153
  // If diagram is very large, provide text file instead
@@ -760,7 +1332,7 @@ program
760
1332
  program
761
1333
  .command('generate [path]')
762
1334
  .description('Generate a diagram')
763
- .option('-t, --type <type>', 'Diagram type: architecture, sequence, dependency, class, flow', 'architecture')
1335
+ .option('-t, --type <type>', 'Diagram type: architecture, sequence, dependency, class, flow, database, user, events, auth, security', 'architecture')
764
1336
  .option('-f, --focus <module>', 'Focus on specific module')
765
1337
  .option('-o, --output <file>', 'Output file (SVG/PNG)')
766
1338
  .option('-m, --max-files <n>', 'Max files to analyze', '100')
@@ -868,18 +1440,123 @@ program
868
1440
 
869
1441
  if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
870
1442
 
871
- const types = ['architecture', 'sequence', 'dependency', 'class', 'flow'];
1443
+ const types = [...SUPPORTED_DIAGRAM_TYPES];
1444
+ const manifest = {
1445
+ generatedAt: new Date().toISOString(),
1446
+ rootPath: root,
1447
+ diagramDir: path.relative(root, outDir) || '.',
1448
+ diagrams: [],
1449
+ };
872
1450
 
873
1451
  for (const type of types) {
874
1452
  const mermaid = generate(data, type);
875
1453
  const file = path.join(outDir, `${type}.mmd`);
876
1454
  fs.writeFileSync(file, mermaid);
1455
+ manifest.diagrams.push(toManifestEntry(type, file, mermaid, root));
877
1456
  console.log(chalk.green('✅'), type, '→', file);
878
1457
  }
1458
+
1459
+ const manifestPath = path.join(outDir, 'manifest.json');
1460
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
1461
+ console.log(chalk.green('✅ manifest'), '→', manifestPath);
879
1462
 
880
1463
  console.log(chalk.cyan('\n🔗 Preview all at: https://mermaid.live'));
881
1464
  });
882
1465
 
1466
+ program
1467
+ .command('manifest [path]')
1468
+ .description('Summarize manifest.json from a diagram output directory')
1469
+ .option('-d, --manifest-dir <dir>', 'Directory containing manifest.json', '.diagram')
1470
+ .option('-o, --output <file>', 'Write summary JSON to a file')
1471
+ .option('--require-types <list>', 'Require all listed diagram types, comma-separated')
1472
+ .option('--fail-on-placeholder', 'Fail if any diagram was a placeholder')
1473
+ .action(async (targetPath, options) => {
1474
+ const root = resolveRootPathOrExit(targetPath);
1475
+ const manifestDir = path.join(root, options.manifestDir || '.diagram');
1476
+ const manifestPath = path.join(manifestDir, 'manifest.json');
1477
+
1478
+ let safeManifestPath;
1479
+ try {
1480
+ safeManifestPath = validateExistingPathInRoot(manifestPath, root, 'manifest path');
1481
+ } catch (err) {
1482
+ console.error(chalk.red('❌ Manifest error:'), err.message);
1483
+ process.exit(2);
1484
+ }
1485
+
1486
+ let manifestRaw;
1487
+ try {
1488
+ manifestRaw = fs.readFileSync(safeManifestPath, 'utf8');
1489
+ } catch (err) {
1490
+ console.error(chalk.red('❌ Manifest read failed:'), err.message);
1491
+ process.exit(2);
1492
+ }
1493
+
1494
+ let parsedManifest;
1495
+ try {
1496
+ parsedManifest = JSON.parse(manifestRaw);
1497
+ } catch (err) {
1498
+ console.error(chalk.red('❌ Manifest parse failed:'), err.message);
1499
+ process.exit(2);
1500
+ }
1501
+
1502
+ const summary = buildManifestSummary(parsedManifest);
1503
+ if (!summary) {
1504
+ console.error(chalk.red('❌ Invalid manifest format'));
1505
+ process.exit(2);
1506
+ }
1507
+
1508
+ const required = parseCommaSeparatedList(options.requireTypes);
1509
+ const missingRequired = required.filter((type) => !summary.diagrams.some((d) => d.type === type));
1510
+ summary.required = {
1511
+ requested: required,
1512
+ missing: missingRequired,
1513
+ };
1514
+
1515
+ if (required.length > 0 && missingRequired.length > 0) {
1516
+ console.error(chalk.red(`❌ Manifest missing required diagram types: ${missingRequired.join(', ')}`));
1517
+ process.exit(2);
1518
+ }
1519
+
1520
+ if (options.failOnPlaceholder && summary.placeholders > 0) {
1521
+ console.error(chalk.yellow(`⚠️ Manifest includes ${summary.placeholders} placeholder diagram(s)`));
1522
+ process.exit(2);
1523
+ }
1524
+
1525
+ if (options.output) {
1526
+ let safeOutput;
1527
+ try {
1528
+ safeOutput = validateOutputPath(options.output, root);
1529
+ } catch (err) {
1530
+ console.error(chalk.red('❌ Output path error:'), err.message);
1531
+ process.exit(2);
1532
+ }
1533
+
1534
+ const outputDir = path.dirname(safeOutput);
1535
+ if (!fs.existsSync(outputDir)) {
1536
+ fs.mkdirSync(outputDir, { recursive: true, mode: 0o755 });
1537
+ }
1538
+
1539
+ fs.writeFileSync(safeOutput, `${JSON.stringify(summary, null, 2)}\n`);
1540
+ console.log(chalk.green('✅ manifest summary'), '→', safeOutput);
1541
+ return;
1542
+ }
1543
+
1544
+ console.log(chalk.blue('\n📘 Manifest summary for'), safeManifestPath);
1545
+ console.log(` Total: ${summary.totalDiagrams}`);
1546
+ console.log(` Placeholder: ${summary.placeholders}`);
1547
+ if (summary.missingTypes.length > 0) {
1548
+ console.log(chalk.yellow(` Missing expected (all supported): ${summary.missingTypes.join(', ')}`));
1549
+ }
1550
+ if (summary.placeholderTypes.length > 0) {
1551
+ console.log(chalk.yellow(` Placeholder types: ${summary.placeholderTypes.join(', ')}`));
1552
+ }
1553
+ console.log('');
1554
+ for (const entry of summary.diagrams) {
1555
+ const status = entry.isPlaceholder ? chalk.yellow('placeholder') : chalk.green('ok');
1556
+ console.log(` ${status} ${entry.type} -> ${entry.file}`);
1557
+ }
1558
+ });
1559
+
883
1560
  program
884
1561
  .command('video [path]')
885
1562
  .description('Generate an animated video of the diagram')