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.
- package/.github/agents/jumpstart-analyst.agent.md +1 -1
- package/.github/agents/jumpstart-architect.agent.md +1 -1
- package/.github/agents/jumpstart-challenger.agent.md +1 -1
- package/.github/agents/jumpstart-developer.agent.md +1 -1
- package/.github/agents/jumpstart-diagram-verifier.agent.md +34 -0
- package/.github/agents/jumpstart-facilitator.agent.md +2 -5
- package/.github/agents/jumpstart-pm.agent.md +1 -1
- package/.github/agents/jumpstart-scout.agent.md +1 -1
- package/.jumpstart/agents/architect.md +64 -9
- package/.jumpstart/agents/diagram-verifier.md +128 -0
- package/.jumpstart/agents/scout.md +245 -27
- package/.jumpstart/commands/commands.md +40 -0
- package/.jumpstart/config.yaml +24 -0
- package/.jumpstart/templates/architecture.md +57 -9
- package/.jumpstart/templates/codebase-context.md +98 -23
- package/bin/cli.js +21 -2
- package/bin/verify-diagrams.js +695 -0
- package/package.json +1 -1
|
@@ -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