jumpstart-mode 1.0.8 → 1.0.9

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.
@@ -0,0 +1,695 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * JumpStart Diagram Verifier — CLI Tool
5
+ *
6
+ * Scans Markdown files in specs/ for Mermaid code blocks and performs
7
+ * structural syntax validation. Designed to catch common authoring mistakes
8
+ * before diagrams reach rendering engines.
9
+ *
10
+ * Usage:
11
+ * npx jumpstart-mode verify [--dir <path>] [--file <path>] [--strict] [--json]
12
+ *
13
+ * Exit codes:
14
+ * 0 — All diagrams pass validation
15
+ * 1 — One or more diagrams have errors
16
+ * 2 — No diagrams found (warning)
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ // ── Colour helpers (graceful fallback) ──────────────────────────────────────
25
+
26
+ let chalk;
27
+ try {
28
+ chalk = require('chalk');
29
+ } catch {
30
+ // Minimal stub when chalk is unavailable
31
+ const id = (s) => s;
32
+ chalk = { red: id, yellow: id, green: id, cyan: id, gray: id, bold: id, dim: id };
33
+ chalk.red.bold = id;
34
+ chalk.green.bold = id;
35
+ }
36
+
37
+ // ── CLI argument parsing ────────────────────────────────────────────────────
38
+
39
+ function parseArgs(argv) {
40
+ const args = { dirs: [], files: [], strict: false, json: false };
41
+ for (let i = 2; i < argv.length; i++) {
42
+ const a = argv[i];
43
+ if (a === '--dir' && argv[i + 1]) {
44
+ args.dirs.push(argv[++i]);
45
+ } else if (a === '--file' && argv[i + 1]) {
46
+ args.files.push(argv[++i]);
47
+ } else if (a === '--strict') {
48
+ args.strict = true;
49
+ } else if (a === '--json') {
50
+ args.json = true;
51
+ } else if (a === '--help' || a === '-h') {
52
+ printHelp();
53
+ process.exit(0);
54
+ }
55
+ }
56
+ // Default to specs/ if nothing specified
57
+ if (args.dirs.length === 0 && args.files.length === 0) {
58
+ args.dirs.push('specs');
59
+ }
60
+ return args;
61
+ }
62
+
63
+ function printHelp() {
64
+ console.log(`
65
+ JumpStart Diagram Verifier
66
+
67
+ Scans Markdown files for Mermaid diagrams and validates syntax.
68
+
69
+ Usage:
70
+ npx jumpstart-mode verify [options]
71
+
72
+ Options:
73
+ --dir <path> Directory to scan (default: specs). Can be repeated.
74
+ --file <path> Single file to scan. Can be repeated.
75
+ --strict Treat warnings as errors.
76
+ --json Output results as JSON.
77
+ -h, --help Show this help message.
78
+
79
+ Examples:
80
+ npx jumpstart-mode verify
81
+ npx jumpstart-mode verify --dir specs --dir docs
82
+ npx jumpstart-mode verify --file specs/architecture.md --strict
83
+ `);
84
+ }
85
+
86
+ // ── File discovery ──────────────────────────────────────────────────────────
87
+
88
+ function findMarkdownFiles(dir) {
89
+ const results = [];
90
+ const base = path.resolve(dir);
91
+ if (!fs.existsSync(base)) return results;
92
+
93
+ function walk(d) {
94
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
95
+ const full = path.join(d, entry.name);
96
+ if (entry.isDirectory()) {
97
+ walk(full);
98
+ } else if (entry.isFile() && /\.md$/i.test(entry.name)) {
99
+ results.push(full);
100
+ }
101
+ }
102
+ }
103
+ walk(base);
104
+ return results;
105
+ }
106
+
107
+ // ── Mermaid block extraction ────────────────────────────────────────────────
108
+
109
+ /**
110
+ * Extracts all ```mermaid ... ``` blocks from a file's content,
111
+ * recording start line, end line, and raw body.
112
+ */
113
+ function extractMermaidBlocks(content) {
114
+ const lines = content.split('\n');
115
+ const blocks = [];
116
+ let inside = false;
117
+ let startLine = 0;
118
+ let body = [];
119
+
120
+ for (let i = 0; i < lines.length; i++) {
121
+ const trimmed = lines[i].trim();
122
+ if (!inside && /^```mermaid\b/i.test(trimmed)) {
123
+ inside = true;
124
+ startLine = i + 1; // 1-based
125
+ body = [];
126
+ } else if (inside && trimmed === '```') {
127
+ blocks.push({
128
+ startLine,
129
+ endLine: i + 1,
130
+ body: body.join('\n'),
131
+ });
132
+ inside = false;
133
+ } else if (inside) {
134
+ body.push(lines[i]);
135
+ }
136
+ }
137
+
138
+ // Unclosed block
139
+ if (inside) {
140
+ blocks.push({
141
+ startLine,
142
+ endLine: lines.length,
143
+ body: body.join('\n'),
144
+ unclosed: true,
145
+ });
146
+ }
147
+
148
+ return blocks;
149
+ }
150
+
151
+ // ── Validation rules ────────────────────────────────────────────────────────
152
+
153
+ const KNOWN_DIAGRAM_TYPES = [
154
+ 'graph', 'flowchart', 'sequenceDiagram', 'classDiagram', 'stateDiagram',
155
+ 'stateDiagram-v2', 'erDiagram', 'journey', 'gantt', 'pie', 'quadrantChart',
156
+ 'requirementDiagram', 'gitGraph', 'mindmap', 'timeline', 'sankey-beta',
157
+ 'xychart-beta', 'block-beta',
158
+ // C4 extension types
159
+ 'C4Context', 'C4Container', 'C4Component', 'C4Dynamic', 'C4Deployment',
160
+ ];
161
+
162
+ const C4_DIAGRAM_TYPES = ['C4Context', 'C4Container', 'C4Component', 'C4Dynamic', 'C4Deployment'];
163
+
164
+ const C4_FUNCTIONS = [
165
+ // Elements: name(alias, label, ?techn, ?descr, ?sprite, ?tags, ?link)
166
+ 'Person', 'Person_Ext',
167
+ 'System', 'System_Ext', 'SystemDb', 'SystemDb_Ext', 'SystemQueue', 'SystemQueue_Ext',
168
+ 'Container', 'Container_Ext', 'ContainerDb', 'ContainerDb_Ext', 'ContainerQueue', 'ContainerQueue_Ext',
169
+ 'Component', 'Component_Ext', 'ComponentDb', 'ComponentDb_Ext', 'ComponentQueue', 'ComponentQueue_Ext',
170
+ // Boundaries
171
+ 'Boundary', 'Enterprise_Boundary', 'System_Boundary', 'Container_Boundary',
172
+ // Relationships
173
+ 'Rel', 'Rel_Back', 'Rel_Neighbor', 'Rel_Back_Neighbor',
174
+ 'Rel_D', 'Rel_Down', 'Rel_U', 'Rel_Up', 'Rel_L', 'Rel_Left', 'Rel_R', 'Rel_Right',
175
+ 'BiRel', 'BiRel_Neighbor', 'BiRel_D', 'BiRel_U', 'BiRel_L', 'BiRel_R',
176
+ // Layout
177
+ 'UpdateElementStyle', 'UpdateRelStyle', 'UpdateLayoutConfig',
178
+ ];
179
+
180
+ /**
181
+ * Validate a single Mermaid block. Returns an array of
182
+ * { level: 'error' | 'warning', message: string, line?: number }.
183
+ */
184
+ function validateBlock(block) {
185
+ const issues = [];
186
+ const { body, startLine, unclosed } = block;
187
+
188
+ if (unclosed) {
189
+ issues.push({ level: 'error', message: 'Unclosed mermaid code block — missing closing ```', line: startLine });
190
+ return issues; // Can't validate further
191
+ }
192
+
193
+ const trimmedBody = body.trim();
194
+ if (!trimmedBody) {
195
+ issues.push({ level: 'error', message: 'Empty mermaid code block', line: startLine });
196
+ return issues;
197
+ }
198
+
199
+ // Determine diagram type from first meaningful line
200
+ const bodyLines = trimmedBody.split('\n');
201
+ const firstLine = bodyLines[0].trim();
202
+ const diagramType = detectDiagramType(firstLine);
203
+
204
+ if (!diagramType) {
205
+ issues.push({
206
+ level: 'error',
207
+ message: `Unrecognised diagram type: "${firstLine.split(/\s/)[0]}". Expected one of: ${KNOWN_DIAGRAM_TYPES.join(', ')}`,
208
+ line: startLine,
209
+ });
210
+ return issues;
211
+ }
212
+
213
+ // ── Generic structural checks ──
214
+
215
+ // Bracket balance (skip for erDiagram — uses { } for entity field blocks with different semantics)
216
+ if (diagramType !== 'erDiagram') {
217
+ const bracketIssues = checkBracketBalance(body, startLine);
218
+ issues.push(...bracketIssues);
219
+ }
220
+
221
+ // Subgraph / end pairing (for graph/flowchart)
222
+ if (['graph', 'flowchart'].includes(diagramType)) {
223
+ issues.push(...checkSubgraphEndPairing(body, startLine));
224
+ issues.push(...checkArrowSyntax(body, startLine));
225
+ }
226
+
227
+ // ── C4-specific checks ──
228
+ if (C4_DIAGRAM_TYPES.includes(diagramType)) {
229
+ issues.push(...checkC4Syntax(body, startLine));
230
+ }
231
+
232
+ // ── erDiagram checks ──
233
+ if (diagramType === 'erDiagram') {
234
+ issues.push(...checkErDiagram(body, startLine));
235
+ }
236
+
237
+ // ── classDiagram checks ──
238
+ if (diagramType === 'classDiagram') {
239
+ issues.push(...checkClassDiagram(body, startLine));
240
+ }
241
+
242
+ return issues;
243
+ }
244
+
245
+ function detectDiagramType(firstLine) {
246
+ for (const dt of KNOWN_DIAGRAM_TYPES) {
247
+ // Match "graph TD", "graph LR", "flowchart TB", "C4Context", etc.
248
+ if (firstLine === dt || firstLine.startsWith(dt + ' ') || firstLine.startsWith(dt + '\t')) {
249
+ return dt;
250
+ }
251
+ }
252
+ return null;
253
+ }
254
+
255
+ // ── Bracket balance ─────────────────────────────────────────────────────────
256
+
257
+ function checkBracketBalance(body, baseLineNum) {
258
+ const issues = [];
259
+ const stacks = { '{': [], '[': [], '(': [] };
260
+ const closers = { '}': '{', ']': '[', ')': '(' };
261
+ const lines = body.split('\n');
262
+
263
+ for (let i = 0; i < lines.length; i++) {
264
+ const line = lines[i];
265
+ // Skip comment lines
266
+ if (line.trim().startsWith('%%')) continue;
267
+ // Skip quoted strings (rough heuristic)
268
+ const unquoted = line.replace(/"[^"]*"/g, '').replace(/'[^']*'/g, '');
269
+
270
+ for (const ch of unquoted) {
271
+ if (ch in stacks) {
272
+ stacks[ch].push(baseLineNum + i);
273
+ } else if (ch in closers) {
274
+ const opener = closers[ch];
275
+ if (stacks[opener].length === 0) {
276
+ issues.push({
277
+ level: 'error',
278
+ message: `Unmatched closing '${ch}'`,
279
+ line: baseLineNum + i,
280
+ });
281
+ } else {
282
+ stacks[opener].pop();
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ for (const [opener, remaining] of Object.entries(stacks)) {
289
+ for (const lineNum of remaining) {
290
+ issues.push({
291
+ level: 'error',
292
+ message: `Unmatched opening '${opener}'`,
293
+ line: lineNum,
294
+ });
295
+ }
296
+ }
297
+
298
+ return issues;
299
+ }
300
+
301
+ // ── Subgraph / end pairing ──────────────────────────────────────────────────
302
+
303
+ function checkSubgraphEndPairing(body, baseLineNum) {
304
+ const issues = [];
305
+ let depth = 0;
306
+ const lines = body.split('\n');
307
+
308
+ for (let i = 0; i < lines.length; i++) {
309
+ const trimmed = lines[i].trim();
310
+ if (/^subgraph\b/i.test(trimmed)) {
311
+ depth++;
312
+ } else if (/^end$/i.test(trimmed)) {
313
+ depth--;
314
+ if (depth < 0) {
315
+ issues.push({ level: 'error', message: 'Unexpected "end" without matching "subgraph"', line: baseLineNum + i });
316
+ depth = 0;
317
+ }
318
+ }
319
+ }
320
+
321
+ if (depth > 0) {
322
+ issues.push({ level: 'error', message: `${depth} unclosed subgraph(s) — missing "end" keyword(s)` });
323
+ }
324
+
325
+ return issues;
326
+ }
327
+
328
+ // ── Arrow syntax for graph/flowchart ────────────────────────────────────────
329
+
330
+ function checkArrowSyntax(body, baseLineNum) {
331
+ const issues = [];
332
+ const lines = body.split('\n');
333
+ const validArrowPattern = /-->|==>|-.->|---->|~~~|---|===|---|--\s|-->/;
334
+
335
+ for (let i = 0; i < lines.length; i++) {
336
+ const trimmed = lines[i].trim();
337
+ if (trimmed.startsWith('%%') || trimmed.startsWith('style') || trimmed.startsWith('class') ||
338
+ trimmed.startsWith('click') || trimmed.startsWith('linkStyle') || /^(graph|flowchart)\s/.test(trimmed) ||
339
+ /^subgraph\b/i.test(trimmed) || /^end$/i.test(trimmed) || !trimmed) {
340
+ continue;
341
+ }
342
+
343
+ // Check if line looks like a node connection but uses invalid syntax
344
+ if (/\w+\s*->\s*\w+/.test(trimmed) && !validArrowPattern.test(trimmed)) {
345
+ issues.push({
346
+ level: 'warning',
347
+ message: `Possible invalid arrow syntax. Use "-->" not "->". Found: "${trimmed.substring(0, 60)}"`,
348
+ line: baseLineNum + i,
349
+ });
350
+ }
351
+ }
352
+
353
+ return issues;
354
+ }
355
+
356
+ // ── C4 syntax checks ────────────────────────────────────────────────────────
357
+
358
+ function checkC4Syntax(body, baseLineNum) {
359
+ const issues = [];
360
+ const lines = body.split('\n');
361
+ const funcPattern = /^\s*(\w+)\s*\(/;
362
+
363
+ for (let i = 0; i < lines.length; i++) {
364
+ const trimmed = lines[i].trim();
365
+ if (!trimmed || trimmed.startsWith('%%') || trimmed.startsWith('title ') ||
366
+ trimmed === '}' || trimmed === '{' || C4_DIAGRAM_TYPES.includes(trimmed)) {
367
+ continue;
368
+ }
369
+
370
+ const funcMatch = trimmed.match(funcPattern);
371
+ if (funcMatch) {
372
+ const funcName = funcMatch[1];
373
+
374
+ // Check if it's a known C4 function
375
+ if (!C4_FUNCTIONS.includes(funcName)) {
376
+ // Only warn if it looks like a function call (not a boundary closing etc.)
377
+ if (trimmed.includes('(') && trimmed.includes(')')) {
378
+ issues.push({
379
+ level: 'warning',
380
+ message: `Unknown C4 function "${funcName}". Known functions: Person, System, Container, Component, Boundary, Rel, etc.`,
381
+ line: baseLineNum + i,
382
+ });
383
+ }
384
+ }
385
+
386
+ // Check minimum argument count for element functions
387
+ if (['Person', 'Person_Ext', 'System', 'System_Ext', 'SystemDb', 'SystemDb_Ext',
388
+ 'Container', 'Container_Ext', 'ContainerDb', 'ContainerDb_Ext',
389
+ 'Component', 'Component_Ext', 'ComponentDb', 'ComponentDb_Ext'].includes(funcName)) {
390
+ const argCount = countArgs(trimmed);
391
+ if (argCount < 2) {
392
+ issues.push({
393
+ level: 'error',
394
+ message: `${funcName}() requires at least 2 arguments (alias, label). Found ${argCount}.`,
395
+ line: baseLineNum + i,
396
+ });
397
+ }
398
+ }
399
+
400
+ // Check Rel minimum arguments
401
+ if (/^(Rel|BiRel)/.test(funcName)) {
402
+ const argCount = countArgs(trimmed);
403
+ if (argCount < 3) {
404
+ issues.push({
405
+ level: 'error',
406
+ message: `${funcName}() requires at least 3 arguments (from, to, label). Found ${argCount}.`,
407
+ line: baseLineNum + i,
408
+ });
409
+ }
410
+ }
411
+
412
+ // Check Boundary minimum arguments
413
+ if (/Boundary$/.test(funcName)) {
414
+ const argCount = countArgs(trimmed);
415
+ if (argCount < 2) {
416
+ issues.push({
417
+ level: 'error',
418
+ message: `${funcName}() requires at least 2 arguments (alias, label). Found ${argCount}.`,
419
+ line: baseLineNum + i,
420
+ });
421
+ }
422
+ }
423
+ }
424
+
425
+ // Warn if graph/flowchart arrows are used in C4 diagrams
426
+ if (/\w+\s*-->?\s*\w+/.test(trimmed) && !trimmed.startsWith('%%')) {
427
+ issues.push({
428
+ level: 'error',
429
+ message: 'Arrow syntax ("-->") is not valid in C4 diagrams. Use Rel() functions instead.',
430
+ line: baseLineNum + i,
431
+ });
432
+ }
433
+
434
+ // Warn about square-bracket nodes in C4
435
+ if (/\w+\[.*\]/.test(trimmed) && !funcMatch && !trimmed.startsWith('%%')) {
436
+ issues.push({
437
+ level: 'warning',
438
+ message: 'Square-bracket node syntax is not valid in C4 diagrams. Use element functions like Person(), System(), Container().',
439
+ line: baseLineNum + i,
440
+ });
441
+ }
442
+ }
443
+
444
+ return issues;
445
+ }
446
+
447
+ /**
448
+ * Count arguments in a function call like: FuncName(arg1, "arg 2", arg3)
449
+ * Handles quoted commas correctly.
450
+ */
451
+ function countArgs(line) {
452
+ const match = line.match(/\(([^)]*)\)/);
453
+ if (!match || !match[1].trim()) return 0;
454
+ const inner = match[1];
455
+
456
+ let count = 1;
457
+ let inQuote = false;
458
+ let quoteChar = '';
459
+ for (const ch of inner) {
460
+ if (!inQuote && (ch === '"' || ch === "'")) {
461
+ inQuote = true;
462
+ quoteChar = ch;
463
+ } else if (inQuote && ch === quoteChar) {
464
+ inQuote = false;
465
+ } else if (!inQuote && ch === ',') {
466
+ count++;
467
+ }
468
+ }
469
+ return count;
470
+ }
471
+
472
+ // ── erDiagram checks ────────────────────────────────────────────────────────
473
+
474
+ function checkErDiagram(body, baseLineNum) {
475
+ const issues = [];
476
+ const lines = body.split('\n');
477
+ const relationPattern = /\S+\s+((\|\||\|o|o\||o\{|\{o|\|\{|\{||\}\||\}o|o\})\s*--\s*(\|\||\|o|o\||o\{|\{o|\|\{|\{||\}\||\}o|o\}))?\s+\S+\s*:/;
478
+ const validCardinality = /(\|\||o\||o\{|\}\||o\}|\{o|\|o|\|\{|\{\||\}o)/;
479
+
480
+ for (let i = 0; i < lines.length; i++) {
481
+ const trimmed = lines[i].trim();
482
+ if (!trimmed || trimmed.startsWith('%%') || trimmed === 'erDiagram') continue;
483
+
484
+ // Check for entity blocks
485
+ if (/^\w+\s*\{/.test(trimmed)) {
486
+ // Entity block opening — valid
487
+ continue;
488
+ }
489
+
490
+ // Check relationship lines
491
+ if (trimmed.includes('--') && trimmed.includes(':')) {
492
+ // Rough check for valid relationship syntax
493
+ if (!relationPattern.test(trimmed)) {
494
+ // Check if cardinality symbols look wrong
495
+ const parts = trimmed.split('--');
496
+ if (parts.length === 2) {
497
+ const leftSide = parts[0].trim();
498
+ const leftSymbol = leftSide.split(/\s+/).pop();
499
+ if (leftSymbol && !validCardinality.test(leftSymbol) && !/^\w+$/.test(leftSymbol)) {
500
+ issues.push({
501
+ level: 'warning',
502
+ message: `Possibly invalid ER relationship cardinality: "${leftSymbol}". Use ||, o|, o{, }|, }o, etc.`,
503
+ line: baseLineNum + i,
504
+ });
505
+ }
506
+ }
507
+ }
508
+ }
509
+ }
510
+
511
+ return issues;
512
+ }
513
+
514
+ // ── classDiagram checks ─────────────────────────────────────────────────────
515
+
516
+ function checkClassDiagram(body, baseLineNum) {
517
+ const issues = [];
518
+ const lines = body.split('\n');
519
+ const validRelationships = /(<\|--|--\*|--o|-->|--\>|\.\.>|\.\.\|>|--|\.\.)/;
520
+
521
+ for (let i = 0; i < lines.length; i++) {
522
+ const trimmed = lines[i].trim();
523
+ if (!trimmed || trimmed.startsWith('%%') || trimmed === 'classDiagram' ||
524
+ trimmed.startsWith('direction') || trimmed.startsWith('note') ||
525
+ trimmed.startsWith('class ') || trimmed.startsWith('<<') ||
526
+ trimmed.startsWith('+') || trimmed.startsWith('-') || trimmed.startsWith('#') ||
527
+ trimmed.startsWith('~') || trimmed === '}') {
528
+ continue;
529
+ }
530
+
531
+ // Check for class member lines inside a class block (indented)
532
+ if (/^\s+[+\-#~]/.test(lines[i])) continue;
533
+
534
+ // Check relationship lines
535
+ if (validRelationships.test(trimmed)) {
536
+ // Valid relationship — check for label
537
+ continue;
538
+ }
539
+ }
540
+
541
+ return issues;
542
+ }
543
+
544
+ // ── Report formatting ───────────────────────────────────────────────────────
545
+
546
+ function formatReport(results, jsonOutput) {
547
+ if (jsonOutput) {
548
+ return JSON.stringify(results, null, 2);
549
+ }
550
+
551
+ const lines = [];
552
+ lines.push('');
553
+ lines.push(chalk.bold('═══════════════════════════════════════════════════════'));
554
+ lines.push(chalk.bold(' JumpStart Diagram Verifier'));
555
+ lines.push(chalk.bold('═══════════════════════════════════════════════════════'));
556
+ lines.push('');
557
+
558
+ let totalDiagrams = 0;
559
+ let totalErrors = 0;
560
+ let totalWarnings = 0;
561
+ let totalPassed = 0;
562
+
563
+ for (const fileResult of results) {
564
+ const relPath = path.relative(process.cwd(), fileResult.file);
565
+ const diagrams = fileResult.diagrams;
566
+
567
+ if (diagrams.length === 0) continue;
568
+
569
+ lines.push(chalk.cyan(`📄 ${relPath}`));
570
+
571
+ for (const diag of diagrams) {
572
+ totalDiagrams++;
573
+ const errors = diag.issues.filter(i => i.level === 'error');
574
+ const warnings = diag.issues.filter(i => i.level === 'warning');
575
+ totalErrors += errors.length;
576
+ totalWarnings += warnings.length;
577
+
578
+ if (errors.length === 0 && warnings.length === 0) {
579
+ totalPassed++;
580
+ lines.push(chalk.green(` ✓ Lines ${diag.startLine}–${diag.endLine}: ${diag.type || 'unknown'} — OK`));
581
+ } else {
582
+ const status = errors.length > 0 ? chalk.red.bold('FAIL') : chalk.yellow('WARN');
583
+ lines.push(` ${status} Lines ${diag.startLine}–${diag.endLine}: ${diag.type || 'unknown'}`);
584
+ for (const issue of diag.issues) {
585
+ const icon = issue.level === 'error' ? chalk.red(' ✗') : chalk.yellow(' ⚠');
586
+ const lineRef = issue.line ? chalk.dim(` (line ${issue.line})`) : '';
587
+ lines.push(` ${icon} ${issue.message}${lineRef}`);
588
+ }
589
+ }
590
+ }
591
+ lines.push('');
592
+ }
593
+
594
+ // Summary
595
+ lines.push(chalk.bold('───────────────────────────────────────────────────────'));
596
+ if (totalDiagrams === 0) {
597
+ lines.push(chalk.yellow(' ⚠ No Mermaid diagrams found in scanned files.'));
598
+ } else {
599
+ lines.push(` Diagrams scanned: ${totalDiagrams}`);
600
+ lines.push(chalk.green(` Passed: ${totalPassed}`));
601
+ if (totalWarnings > 0) lines.push(chalk.yellow(` Warnings: ${totalWarnings}`));
602
+ if (totalErrors > 0) lines.push(chalk.red(` Errors: ${totalErrors}`));
603
+
604
+ if (totalErrors === 0 && totalWarnings === 0) {
605
+ lines.push('');
606
+ lines.push(chalk.green.bold(' ✓ All diagrams passed verification.'));
607
+ } else if (totalErrors === 0) {
608
+ lines.push('');
609
+ lines.push(chalk.yellow(' ⚠ All diagrams structurally valid, but some warnings found.'));
610
+ } else {
611
+ lines.push('');
612
+ lines.push(chalk.red.bold(' ✗ Diagram verification failed. Fix errors above.'));
613
+ }
614
+ }
615
+ lines.push(chalk.bold('═══════════════════════════════════════════════════════'));
616
+ lines.push('');
617
+
618
+ return lines.join('\n');
619
+ }
620
+
621
+ // ── Main entry ──────────────────────────────────────────────────────────────
622
+
623
+ function run(argv) {
624
+ const args = parseArgs(argv || process.argv);
625
+
626
+ // Gather files
627
+ let files = [...args.files.map(f => path.resolve(f))];
628
+ for (const dir of args.dirs) {
629
+ files.push(...findMarkdownFiles(dir));
630
+ }
631
+
632
+ // Dedupe
633
+ files = [...new Set(files)];
634
+
635
+ if (files.length === 0) {
636
+ if (args.json) {
637
+ console.log(JSON.stringify({ diagrams: 0, errors: 0, warnings: 0, files: [] }));
638
+ } else {
639
+ console.log(chalk.yellow('\n ⚠ No Markdown files found to scan.\n'));
640
+ }
641
+ process.exit(2);
642
+ }
643
+
644
+ // Scan each file
645
+ const results = [];
646
+ for (const file of files) {
647
+ if (!fs.existsSync(file)) {
648
+ results.push({ file, error: 'File not found', diagrams: [] });
649
+ continue;
650
+ }
651
+
652
+ const content = fs.readFileSync(file, 'utf8');
653
+ const blocks = extractMermaidBlocks(content);
654
+
655
+ const diagrams = blocks.map(block => {
656
+ const issues = validateBlock(block);
657
+ const firstLine = block.body.trim().split('\n')[0] || '';
658
+ const type = detectDiagramType(firstLine.trim()) || firstLine.split(/\s/)[0] || 'unknown';
659
+ return {
660
+ startLine: block.startLine,
661
+ endLine: block.endLine,
662
+ type,
663
+ issues,
664
+ };
665
+ });
666
+
667
+ results.push({ file, diagrams });
668
+ }
669
+
670
+ // Output
671
+ console.log(formatReport(results, args.json));
672
+
673
+ // Determine exit code
674
+ let hasErrors = false;
675
+ let hasDiagrams = false;
676
+ for (const r of results) {
677
+ for (const d of r.diagrams) {
678
+ hasDiagrams = true;
679
+ for (const issue of d.issues) {
680
+ if (issue.level === 'error') hasErrors = true;
681
+ if (args.strict && issue.level === 'warning') hasErrors = true;
682
+ }
683
+ }
684
+ }
685
+
686
+ if (!hasDiagrams) process.exit(2);
687
+ process.exit(hasErrors ? 1 : 0);
688
+ }
689
+
690
+ // Allow both CLI and programmatic usage
691
+ module.exports = { run, extractMermaidBlocks, validateBlock, detectDiagramType };
692
+
693
+ if (require.main === module) {
694
+ run();
695
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jumpstart-mode",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Spec-driven agentic coding framework that transforms ideas into production code through AI-powered sequential phases",
5
5
  "keywords": [
6
6
  "jumpstart",