@tekyzinc/gsd-t 2.33.12 → 2.34.12
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/README.md +1 -0
- package/bin/gsd-t.js +8 -0
- package/bin/scan-data-collector.js +153 -0
- package/bin/scan-diagrams-generators.js +187 -0
- package/bin/scan-diagrams.js +79 -0
- package/bin/scan-export.js +49 -0
- package/bin/scan-renderer.js +92 -0
- package/bin/scan-report-sections.js +121 -0
- package/bin/scan-report.js +181 -0
- package/bin/scan-schema-parsers.js +199 -0
- package/bin/scan-schema.js +103 -0
- package/commands/gsd-t-impact.md +33 -1
- package/commands/gsd-t-partition.md +72 -0
- package/commands/gsd-t-plan.md +31 -0
- package/commands/gsd-t-scan.md +88 -1
- package/docs/architecture.md +16 -2
- package/docs/infrastructure.md +23 -6
- package/docs/requirements.md +119 -1
- package/docs/workflows.md +67 -1
- package/package.json +1 -1
- package/templates/shared-services-contract.md +60 -0
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ A methodology for reliable, parallelizable development using Claude Code with op
|
|
|
8
8
|
**Catches downstream effects** — analyzes impact before changes break things.
|
|
9
9
|
**Protects existing work** — destructive action guard prevents schema drops, architecture replacements, and data loss without explicit approval.
|
|
10
10
|
**Visualizes execution in real time** — live browser dashboard renders agent hierarchy, tool activity, and phase progression from the event stream.
|
|
11
|
+
**Generates visual scan reports** — every `/gsd-t-scan` produces a self-contained HTML report with 6 live architectural diagrams, a tech debt register, and domain health scores; optional DOCX/PDF export via `--export docx|pdf`.
|
|
11
12
|
|
|
12
13
|
---
|
|
13
14
|
|
package/bin/gsd-t.js
CHANGED
|
@@ -1482,6 +1482,14 @@ if (require.main === module) {
|
|
|
1482
1482
|
case "changelog":
|
|
1483
1483
|
doChangelog();
|
|
1484
1484
|
break;
|
|
1485
|
+
case "scan": {
|
|
1486
|
+
const exportFlag = args.find(a => a.startsWith('--export='));
|
|
1487
|
+
const exportFormat = exportFlag ? exportFlag.split('=')[1] : null;
|
|
1488
|
+
if (exportFormat) {
|
|
1489
|
+
log(`${CYAN} ℹ${RESET} Export flag noted: will export to ${exportFormat} after scan completes`);
|
|
1490
|
+
}
|
|
1491
|
+
break;
|
|
1492
|
+
}
|
|
1485
1493
|
case "help":
|
|
1486
1494
|
case "--help":
|
|
1487
1495
|
case "-h":
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
function read(filePath) {
|
|
6
|
+
try { return fs.readFileSync(filePath, 'utf8'); } catch { return ''; }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function parseDebtSummary(text) {
|
|
10
|
+
const n = (pattern) => { const m = text.match(pattern); return m ? parseInt(m[1], 10) : 0; };
|
|
11
|
+
return {
|
|
12
|
+
debtCritical: n(/Critical items?:\s*(\d+)/i),
|
|
13
|
+
debtHigh: n(/High priority:\s*(\d+)/i),
|
|
14
|
+
debtMedium: n(/Medium priority:\s*(\d+)/i)
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseTestCoverage(text) {
|
|
19
|
+
const total = text.match(/Total tests\s*\|\s*(\d+)/i);
|
|
20
|
+
const passing = text.match(/Passing\s*\|\s*(\d+)/i);
|
|
21
|
+
if (total && passing) return parseInt(passing[1], 10) + '/' + parseInt(total[1], 10);
|
|
22
|
+
return 'N/A';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseFilesAndLoc(text) {
|
|
26
|
+
const m = text.match(/\|\s*\*?\*?Total[^|]*\*?\*?\s*\|\s*(\d+)\s+files?\s*\|\s*\*?\*?([\d,]+)[^|]*\*?\*?\s*\|/i);
|
|
27
|
+
if (m) return { filesScanned: parseInt(m[1], 10), totalLoc: parseInt(m[2].replace(/,/g, ''), 10) };
|
|
28
|
+
return { filesScanned: 0, totalLoc: 0 };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseComponents(text) {
|
|
32
|
+
const sec = text.match(/## Component Inventory([\s\S]*?)(?=\n## |\n---|\n#[^#]|$)/);
|
|
33
|
+
if (!sec) return [];
|
|
34
|
+
return sec[1].split('\n')
|
|
35
|
+
.filter(l => /^\|/.test(l) && !/---/.test(l) && !/Component.*File/i.test(l))
|
|
36
|
+
.map(row => {
|
|
37
|
+
const cols = row.split('|').map(c => c.trim().replace(/\*\*/g, '').replace(/`/g, '')).filter(Boolean);
|
|
38
|
+
if (cols.length < 3) return null;
|
|
39
|
+
const name = cols[0];
|
|
40
|
+
if (!name || /^total/i.test(name)) return null;
|
|
41
|
+
return { name, filePath: cols[1] || '', size: cols[2] || '', purpose: cols[3] || '', files: 1, healthScore: 80 };
|
|
42
|
+
})
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseSeverityMap(text) {
|
|
47
|
+
const map = {};
|
|
48
|
+
const high = text.match(/High priority:[^\n]*\(([^)]+)\)/i);
|
|
49
|
+
if (high) {
|
|
50
|
+
high[1].split(',').forEach(part => { const m = part.trim().match(/TD-\d+/); if (m) map[m[0]] = 'high'; });
|
|
51
|
+
}
|
|
52
|
+
const med = text.match(/Medium priority:[^\n]*\(([^)]+)\)/i);
|
|
53
|
+
if (med) {
|
|
54
|
+
med[1].split(',').forEach(part => {
|
|
55
|
+
const r = part.trim().match(/TD-(\d+)[–\-](\d+)/);
|
|
56
|
+
if (r) {
|
|
57
|
+
for (let i = parseInt(r[1]); i <= parseInt(r[2]); i++) {
|
|
58
|
+
map['TD-' + String(i).padStart(3, '0')] = 'medium';
|
|
59
|
+
}
|
|
60
|
+
} else { const m = part.trim().match(/TD-\d+/); if (m) map[m[0]] = 'medium'; }
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return map;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseTechDebtItems(qualText, debtText) {
|
|
67
|
+
if (!qualText) return [];
|
|
68
|
+
const severityMap = parseSeverityMap(debtText || '');
|
|
69
|
+
const tableMatch = qualText.match(/\| ID \| Title \| Status \|([\s\S]*?)(?=\n---|\n## |$)/i);
|
|
70
|
+
if (!tableMatch) return [];
|
|
71
|
+
return tableMatch[1].split('\n')
|
|
72
|
+
.filter(l => /^\|/.test(l) && !/---/.test(l) && !/\| ID \|/i.test(l))
|
|
73
|
+
.map(row => {
|
|
74
|
+
const cols = row.split('|').map(c => c.trim()).filter(Boolean);
|
|
75
|
+
if (cols.length < 3) return null;
|
|
76
|
+
const id = cols[0]; const title = cols[1];
|
|
77
|
+
if (!cols[2].toUpperCase().includes('OPEN')) return null;
|
|
78
|
+
return { severity: severityMap[id] || 'low', domain: id, issue: title, location: '', effort: '' };
|
|
79
|
+
})
|
|
80
|
+
.filter(Boolean).slice(0, 20);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseSecurityFindings(text) {
|
|
84
|
+
if (!text) return [];
|
|
85
|
+
const findings = [];
|
|
86
|
+
for (const sec of text.split(/\n### /).slice(1)) {
|
|
87
|
+
const titleLine = sec.split('\n')[0];
|
|
88
|
+
if (!/SEC-[HM]\d+/.test(titleLine)) continue;
|
|
89
|
+
const idM = titleLine.match(/(SEC-[HM]\d+)/);
|
|
90
|
+
const nameM = titleLine.match(/SEC-[HM]\d+:\s*(.+?)(?:\s+[-–][-–]|\s*$)/);
|
|
91
|
+
const detM = sec.match(/- \*\*Details\*\*:\s*(.+?)(?=\n-|\n\n|$)/s);
|
|
92
|
+
const fixM = sec.match(/- \*\*Fix\*\*:\s*(.+?)(?=\n-|\n\n|$)/s);
|
|
93
|
+
findings.push({
|
|
94
|
+
category: /SEC-H/.test(titleLine) ? 'Security' : 'Security',
|
|
95
|
+
severity: /SEC-H/.test(titleLine) ? 'high' : 'medium',
|
|
96
|
+
title: (idM ? idM[1] : '') + (nameM ? ': ' + nameM[1].trim() : ''),
|
|
97
|
+
description: detM ? detM[1].trim().replace(/\n/g, ' ') : '',
|
|
98
|
+
recommendation: fixM ? fixM[1].trim().replace(/\n/g, ' ') : ''
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return findings;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseQualityFindings(text) {
|
|
105
|
+
if (!text) return [];
|
|
106
|
+
const findings = [];
|
|
107
|
+
for (const sec of text.split(/\n### /).slice(1)) {
|
|
108
|
+
const titleLine = sec.split('\n')[0];
|
|
109
|
+
const idM = titleLine.match(/((?:DC|TCG|TD)-[A-Z\-\d]+)/);
|
|
110
|
+
const nameM = titleLine.match(/(?:DC|TCG|TD)-[A-Z\-\d]+:\s*(.+?)(?:\s*$)/);
|
|
111
|
+
const locM = sec.match(/^`([^`]+)`/m);
|
|
112
|
+
const detM = sec.match(/\n(.+?)\n- \*\*Impact\*\*/s);
|
|
113
|
+
const sugM = sec.match(/- \*\*Suggestion\*\*:\s*(.+?)(?=\n-|\n\n|$)/s);
|
|
114
|
+
if (!idM) continue;
|
|
115
|
+
findings.push({
|
|
116
|
+
category: 'Quality',
|
|
117
|
+
severity: 'medium',
|
|
118
|
+
title: (idM ? idM[1] : '') + (nameM ? ': ' + nameM[1].trim() : ''),
|
|
119
|
+
description: locM ? locM[1] : (detM ? detM[1].trim() : ''),
|
|
120
|
+
recommendation: sugM ? sugM[1].trim().replace(/\n/g, ' ') : 'Review and schedule remediation'
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return findings.slice(0, 3);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function collectScanData(projectRoot) {
|
|
127
|
+
const scanDir = path.join(projectRoot, '.gsd-t', 'scan');
|
|
128
|
+
const rs = (f) => read(path.join(scanDir, f));
|
|
129
|
+
const rr = (f) => read(path.join(projectRoot, f));
|
|
130
|
+
|
|
131
|
+
const archText = rs('architecture.md');
|
|
132
|
+
const testText = rs('test-baseline.md');
|
|
133
|
+
const secText = rs('security.md');
|
|
134
|
+
const qualText = rs('quality.md');
|
|
135
|
+
const debtText = rr('.gsd-t/techdebt.md');
|
|
136
|
+
|
|
137
|
+
let projectName = path.basename(projectRoot);
|
|
138
|
+
try { projectName = JSON.parse(rr('package.json')).name || projectName; } catch {}
|
|
139
|
+
|
|
140
|
+
const { filesScanned, totalLoc } = parseFilesAndLoc(archText);
|
|
141
|
+
const { debtCritical, debtHigh, debtMedium } = parseDebtSummary(debtText);
|
|
142
|
+
const testCoverage = parseTestCoverage(testText);
|
|
143
|
+
const domains = parseComponents(archText);
|
|
144
|
+
const techDebt = parseTechDebtItems(qualText, debtText);
|
|
145
|
+
const secFinds = parseSecurityFindings(secText);
|
|
146
|
+
const qualFinds = parseQualityFindings(qualText);
|
|
147
|
+
const findings = secFinds.concat(qualFinds).slice(0, 10);
|
|
148
|
+
|
|
149
|
+
return { projectName, filesScanned, totalLoc, debtCritical, debtHigh, debtMedium,
|
|
150
|
+
testCoverage, domains, techDebt, findings };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = { collectScanData };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function genSystemArchitecture(analysisData) {
|
|
4
|
+
try {
|
|
5
|
+
const services = (analysisData.services || []).slice(0, 4);
|
|
6
|
+
if (services.length >= 2) {
|
|
7
|
+
const lines = ['graph TB',
|
|
8
|
+
' classDef user fill:#0d2035,stroke:#06b6d4,color:#a5f3fc',
|
|
9
|
+
' classDef svc fill:#1a0f3a,stroke:#7c3aed,color:#ddd6fe',
|
|
10
|
+
' classDef db fill:#0a2318,stroke:#10b981,color:#a7f3d0',
|
|
11
|
+
' User(["👤 User"]):::user'];
|
|
12
|
+
services.forEach((s, i) => { lines.push(' S' + i + '["' + s + '"]:::svc'); lines.push(' User --> S' + i); });
|
|
13
|
+
return lines.join('\n');
|
|
14
|
+
}
|
|
15
|
+
return `graph TB
|
|
16
|
+
classDef user fill:#0d2035,stroke:#06b6d4,color:#a5f3fc
|
|
17
|
+
classDef app fill:#0f1d3a,stroke:#3b82f6,color:#bfdbfe
|
|
18
|
+
classDef api fill:#1a0f3a,stroke:#7c3aed,color:#ddd6fe
|
|
19
|
+
classDef db fill:#0a2318,stroke:#10b981,color:#a7f3d0
|
|
20
|
+
classDef ext fill:#111827,stroke:#374151,color:#9ca3af
|
|
21
|
+
USER(["👤 User\\nweb & mobile"]):::user
|
|
22
|
+
subgraph PLATFORM[" Application Platform "]
|
|
23
|
+
APP["🌐 Frontend\\nUser Interface"]:::app
|
|
24
|
+
API["⚡ Backend\\nApplication Logic"]:::api
|
|
25
|
+
DB[("🗃 Database\\nPrimary Store")]:::db
|
|
26
|
+
end
|
|
27
|
+
EXT["🌎 External Services\\nAPIs & Integrations"]:::ext
|
|
28
|
+
USER -->|"HTTPS"| APP
|
|
29
|
+
APP -->|"REST / JSON"| API
|
|
30
|
+
API -->|"reads / writes"| DB
|
|
31
|
+
API -->|"calls"| EXT
|
|
32
|
+
style PLATFORM fill:#0a0f1e,stroke:#1e3a5f,color:#e2e8f0`;
|
|
33
|
+
} catch { return 'graph TB\n App[Application] --> DB[(Database)]'; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function genAppArchitecture(analysisData) {
|
|
37
|
+
try {
|
|
38
|
+
const layers = (analysisData.layers || []).slice(0, 5);
|
|
39
|
+
if (layers.length >= 3) {
|
|
40
|
+
const lines = ['graph TB'];
|
|
41
|
+
const colors = ['#0f1d3a,#3b82f6', '#1a0f3a,#7c3aed', '#0a2318,#10b981', '#1f1505,#f59e0b', '#12102a,#6366f1'];
|
|
42
|
+
layers.forEach((l, i) => {
|
|
43
|
+
const [bg, stroke] = colors[i % colors.length].split(',');
|
|
44
|
+
lines.push(` L${i}["${l}"]`);
|
|
45
|
+
lines.push(` style L${i} fill:${bg},stroke:${stroke},color:#e2e8f0`);
|
|
46
|
+
if (i > 0) lines.push(` L${i - 1} --> L${i}`);
|
|
47
|
+
});
|
|
48
|
+
return lines.join('\n');
|
|
49
|
+
}
|
|
50
|
+
return `graph TB
|
|
51
|
+
classDef client fill:#0d2035,stroke:#06b6d4,color:#a5f3fc
|
|
52
|
+
classDef ctrl fill:#0f1d3a,stroke:#3b82f6,color:#bfdbfe
|
|
53
|
+
classDef svc fill:#1a0f3a,stroke:#7c3aed,color:#ddd6fe
|
|
54
|
+
classDef repo fill:#1a0f3a,stroke:#8b5cf6,color:#ddd6fe
|
|
55
|
+
classDef db fill:#0a2318,stroke:#10b981,color:#a7f3d0
|
|
56
|
+
subgraph CL[" Clients "]
|
|
57
|
+
WEB["🌐 Web App"]:::client
|
|
58
|
+
MOB["📱 Mobile"]:::client
|
|
59
|
+
end
|
|
60
|
+
subgraph CTR[" Controller Layer "]
|
|
61
|
+
AC["AuthController"]:::ctrl
|
|
62
|
+
TC["TasksController"]:::ctrl
|
|
63
|
+
PC["ProjectsController"]:::ctrl
|
|
64
|
+
end
|
|
65
|
+
subgraph SVC[" Service Layer "]
|
|
66
|
+
AS["AuthService"]:::svc
|
|
67
|
+
TS["TasksService"]:::svc
|
|
68
|
+
PS["ProjectsService"]:::svc
|
|
69
|
+
end
|
|
70
|
+
DB[("🗃 Database")]:::db
|
|
71
|
+
WEB & MOB --> CTR --> SVC --> DB
|
|
72
|
+
style CL fill:#080e1a,stroke:#0e2035
|
|
73
|
+
style CTR fill:#080e1a,stroke:#1e3a5f
|
|
74
|
+
style SVC fill:#080e1a,stroke:#2d1a5e`;
|
|
75
|
+
} catch { return 'graph TB\n subgraph App\n Controllers --> Services --> Repositories\n end'; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function genWorkflow(analysisData) {
|
|
79
|
+
try {
|
|
80
|
+
const states = (analysisData.states || []).slice(0, 6);
|
|
81
|
+
if (states.length >= 3) {
|
|
82
|
+
const lines = ['stateDiagram-v2', ' direction LR', ' [*] --> ' + states[0]];
|
|
83
|
+
states.forEach((s, i) => { if (i < states.length - 1) lines.push(` ${s} --> ${states[i + 1]}`); });
|
|
84
|
+
lines.push(' ' + states[states.length - 1] + ' --> [*]');
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
}
|
|
87
|
+
return `stateDiagram-v2
|
|
88
|
+
direction LR
|
|
89
|
+
[*] --> Draft : create
|
|
90
|
+
Draft --> Open : submit
|
|
91
|
+
Draft --> [*] : discard
|
|
92
|
+
Open --> InProgress : assign
|
|
93
|
+
Open --> Cancelled : cancel
|
|
94
|
+
InProgress --> Review : mark done
|
|
95
|
+
InProgress --> Blocked : flag blocker
|
|
96
|
+
InProgress --> Open : unassign
|
|
97
|
+
Blocked --> InProgress : resolve
|
|
98
|
+
Review --> Done : approve
|
|
99
|
+
Review --> InProgress : reject
|
|
100
|
+
Done --> [*]
|
|
101
|
+
Cancelled --> [*]
|
|
102
|
+
note right of Blocked
|
|
103
|
+
No SLA timeout —
|
|
104
|
+
can stagnate here
|
|
105
|
+
end note`;
|
|
106
|
+
} catch { return 'stateDiagram-v2\n [*] --> Active\n Active --> Done\n Done --> [*]'; }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function genDataFlow(analysisData) {
|
|
110
|
+
try {
|
|
111
|
+
const endpoints = (analysisData.endpoints || []).slice(0, 1);
|
|
112
|
+
const ep = endpoints[0] || 'POST /api/resource';
|
|
113
|
+
return `flowchart TD
|
|
114
|
+
classDef user fill:#0d2035,stroke:#06b6d4,color:#a5f3fc
|
|
115
|
+
classDef fe fill:#0f1d3a,stroke:#3b82f6,color:#bfdbfe
|
|
116
|
+
classDef api fill:#1a0f3a,stroke:#7c3aed,color:#ddd6fe
|
|
117
|
+
classDef db fill:#0a2318,stroke:#10b981,color:#a7f3d0
|
|
118
|
+
classDef queue fill:#1f1505,stroke:#f59e0b,color:#fde68a
|
|
119
|
+
classDef ok fill:#0a2318,stroke:#10b981,color:#a7f3d0
|
|
120
|
+
USR(["👤 User"]):::user
|
|
121
|
+
subgraph FE["Frontend"]
|
|
122
|
+
FORM["Form\\n+ Client Validation"]:::fe
|
|
123
|
+
end
|
|
124
|
+
subgraph API["API Server"]
|
|
125
|
+
PIPE["ValidationPipe\\n+ Sanitize"]:::api
|
|
126
|
+
CTRL["Controller\\n${ep}"]:::api
|
|
127
|
+
SVC["Service\\nbusiness logic"]:::api
|
|
128
|
+
REPO["Repository\\nINSERT / UPDATE"]:::api
|
|
129
|
+
end
|
|
130
|
+
DB[("🗃 Database")]:::db
|
|
131
|
+
RD[("⚡ Queue")]:::queue
|
|
132
|
+
RES(["✓ Response"]):::ok
|
|
133
|
+
USR --> FORM --> PIPE --> CTRL --> SVC --> REPO --> DB
|
|
134
|
+
SVC --> RD
|
|
135
|
+
DB --> RES
|
|
136
|
+
style FE fill:#080e1a,stroke:#1e3a5f
|
|
137
|
+
style API fill:#080e1a,stroke:#2d1a5e`;
|
|
138
|
+
} catch { return 'flowchart TD\n Input --> Validate --> Process --> Store --> Respond'; }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function genSequence(analysisData) {
|
|
142
|
+
try {
|
|
143
|
+
const ep = ((analysisData.endpoints || [])[0]) || 'POST /api/resource';
|
|
144
|
+
return `sequenceDiagram
|
|
145
|
+
autonumber
|
|
146
|
+
actor User
|
|
147
|
+
participant Client
|
|
148
|
+
participant API as API Server
|
|
149
|
+
participant DB as Database
|
|
150
|
+
participant Queue
|
|
151
|
+
User->>Client: Submit form
|
|
152
|
+
Client->>API: ${ep}
|
|
153
|
+
API->>API: validate & sanitize
|
|
154
|
+
alt invalid input
|
|
155
|
+
API-->>Client: 400 Bad Request
|
|
156
|
+
else valid
|
|
157
|
+
API->>DB: INSERT record
|
|
158
|
+
DB-->>API: record id
|
|
159
|
+
API->>Queue: enqueue background job
|
|
160
|
+
API-->>Client: 201 Created
|
|
161
|
+
Client-->>User: Success feedback
|
|
162
|
+
end`;
|
|
163
|
+
} catch { return 'sequenceDiagram\n Client->>Server: Request\n Server->>DB: Query\n DB-->>Server: Result\n Server-->>Client: Response'; }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function genDatabaseSchema(schemaData) {
|
|
167
|
+
try {
|
|
168
|
+
if (!schemaData || !schemaData.detected || !schemaData.entities || schemaData.entities.length === 0) return '';
|
|
169
|
+
const relMap = { 'one-to-many': '||--o{', 'many-to-one': '}o--||', 'many-to-many': '}o--o{', 'one-to-one': '||--||' };
|
|
170
|
+
const lines = ['erDiagram'];
|
|
171
|
+
for (const entity of schemaData.entities.slice(0, 8)) {
|
|
172
|
+
lines.push(' ' + entity.name + ' {');
|
|
173
|
+
for (const f of (entity.fields || []).slice(0, 10)) {
|
|
174
|
+
lines.push(' ' + (f.type || 'string') + ' ' + f.name);
|
|
175
|
+
}
|
|
176
|
+
lines.push(' }');
|
|
177
|
+
}
|
|
178
|
+
for (const entity of schemaData.entities) {
|
|
179
|
+
for (const rel of (entity.relations || [])) {
|
|
180
|
+
lines.push(' ' + rel.fromEntity + ' ' + (relMap[rel.type] || '||--o{') + ' ' + rel.toEntity + ' : "has"');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return lines.join('\n');
|
|
184
|
+
} catch { return ''; }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = { genSystemArchitecture, genAppArchitecture, genWorkflow, genDataFlow, genSequence, genDatabaseSchema };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
genSystemArchitecture,
|
|
5
|
+
genAppArchitecture,
|
|
6
|
+
genWorkflow,
|
|
7
|
+
genDataFlow,
|
|
8
|
+
genSequence,
|
|
9
|
+
genDatabaseSchema
|
|
10
|
+
} = require('./scan-diagrams-generators');
|
|
11
|
+
|
|
12
|
+
const PLACEHOLDER_HTML = '<div class="diagram-placeholder">\n <p>Diagram unavailable — rendering tools not found</p>\n <p>Install: <code>npm install -g @mermaid-js/mermaid-cli</code></p>\n</div>';
|
|
13
|
+
|
|
14
|
+
const NOTES = {
|
|
15
|
+
'system-architecture': 'C4-style context diagram showing services, databases, and external integrations',
|
|
16
|
+
'app-architecture': 'Layered diagram showing framework architecture and component boundaries',
|
|
17
|
+
'workflow': 'State machine derived from status enums and state transition logic',
|
|
18
|
+
'data-flow': 'Data flow from user input through validation, persistence, and async processing',
|
|
19
|
+
'sequence': 'Request/response sequence for the primary API endpoint',
|
|
20
|
+
'database-schema': 'Schema diagram generated from ORM, document DB, vector store, or raw SQL definitions'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const DIAGRAM_DEFS = [
|
|
24
|
+
{ type: 'system-architecture', title: 'System Architecture', typeBadge: 'graph TB', gen: (a) => genSystemArchitecture(a) },
|
|
25
|
+
{ type: 'app-architecture', title: 'Application Architecture', typeBadge: 'graph TB', gen: (a) => genAppArchitecture(a) },
|
|
26
|
+
{ type: 'workflow', title: 'Workflow', typeBadge: 'stateDiagram-v2', gen: (a) => genWorkflow(a) },
|
|
27
|
+
{ type: 'data-flow', title: 'Data Flow', typeBadge: 'flowchart TD', gen: (a) => genDataFlow(a) },
|
|
28
|
+
{ type: 'sequence', title: 'Sequence', typeBadge: 'sequenceDiagram', gen: (a) => genSequence(a) },
|
|
29
|
+
{ type: 'database-schema', title: 'Database Schema', typeBadge: 'erDiagram', gen: (_, s) => genDatabaseSchema(s) }
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function buildPlaceholder(def, mmd) {
|
|
33
|
+
return {
|
|
34
|
+
type: def.type,
|
|
35
|
+
title: def.title,
|
|
36
|
+
typeBadge: def.typeBadge,
|
|
37
|
+
svgContent: PLACEHOLDER_HTML,
|
|
38
|
+
mmdSource: mmd || '',
|
|
39
|
+
note: NOTES[def.type] || '',
|
|
40
|
+
rendered: false,
|
|
41
|
+
rendererUsed: 'placeholder'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function generateDiagrams(analysisData, schemaData, options) {
|
|
46
|
+
try {
|
|
47
|
+
const { renderDiagram } = require('./scan-renderer');
|
|
48
|
+
const results = [];
|
|
49
|
+
for (const def of DIAGRAM_DEFS) {
|
|
50
|
+
try {
|
|
51
|
+
const mmd = def.gen(analysisData, schemaData);
|
|
52
|
+
const isDbSchema = def.type === 'database-schema';
|
|
53
|
+
const noSchema = !schemaData || !schemaData.detected || !mmd;
|
|
54
|
+
if (isDbSchema && noSchema) {
|
|
55
|
+
results.push(buildPlaceholder(def, ''));
|
|
56
|
+
} else {
|
|
57
|
+
const rendered = renderDiagram(mmd, def.type, options || {});
|
|
58
|
+
results.push({
|
|
59
|
+
type: def.type,
|
|
60
|
+
title: def.title,
|
|
61
|
+
typeBadge: def.typeBadge,
|
|
62
|
+
svgContent: rendered.svgContent,
|
|
63
|
+
mmdSource: mmd,
|
|
64
|
+
note: NOTES[def.type] || '',
|
|
65
|
+
rendered: rendered.rendered,
|
|
66
|
+
rendererUsed: rendered.rendererUsed
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
results.push(buildPlaceholder(def, ''));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return results;
|
|
74
|
+
} catch {
|
|
75
|
+
return DIAGRAM_DEFS.map(def => buildPlaceholder(def));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { generateDiagrams };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { execSync } = require('child_process');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
function detectTool(cmd) {
|
|
6
|
+
try {
|
|
7
|
+
const check = process.platform === 'win32' ? 'where "' + cmd + '"' : 'which "' + cmd + '"';
|
|
8
|
+
execSync(check, { stdio: 'pipe', timeout: 5000 });
|
|
9
|
+
return true;
|
|
10
|
+
} catch { return false; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function detectMdToPdf() {
|
|
14
|
+
try { execSync('npx md-to-pdf --version', { stdio: 'pipe', timeout: 10000 }); return true; }
|
|
15
|
+
catch { return false; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function exportToDocx(htmlPath) {
|
|
19
|
+
try {
|
|
20
|
+
const outputPath = htmlPath.replace(/\.html$/, '.docx');
|
|
21
|
+
execSync('pandoc "' + htmlPath + '" -o "' + outputPath + '" --from=html', { timeout: 60000 });
|
|
22
|
+
return { success: true, outputPath };
|
|
23
|
+
} catch (err) { return { success: false, error: err.message }; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function exportToPdf(htmlPath) {
|
|
27
|
+
try {
|
|
28
|
+
const outputPath = htmlPath.replace(/\.html$/, '.pdf');
|
|
29
|
+
execSync('npx md-to-pdf "' + htmlPath + '" --output "' + outputPath + '"', { timeout: 120000 });
|
|
30
|
+
return { success: true, outputPath };
|
|
31
|
+
} catch (err) { return { success: false, error: err.message }; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function exportReport(htmlPath, format, options) {
|
|
35
|
+
try {
|
|
36
|
+
if (format !== 'docx' && format !== 'pdf') {
|
|
37
|
+
return { success: false, error: 'Unknown export format: ' + format + '. Use docx or pdf.' };
|
|
38
|
+
}
|
|
39
|
+
if (format === 'docx' && !detectTool('pandoc')) {
|
|
40
|
+
return { success: false, skipped: true, reason: 'Pandoc not found. Install: https://pandoc.org/installing.html' };
|
|
41
|
+
}
|
|
42
|
+
if (format === 'pdf' && !detectMdToPdf()) {
|
|
43
|
+
return { success: false, skipped: true, reason: 'md-to-pdf not found. Install: npm install -g md-to-pdf' };
|
|
44
|
+
}
|
|
45
|
+
return format === 'docx' ? exportToDocx(htmlPath, options) : exportToPdf(htmlPath, options);
|
|
46
|
+
} catch (err) { return { success: false, error: err.message }; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { exportReport };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const https = require('https');
|
|
7
|
+
|
|
8
|
+
const PLACEHOLDER_HTML = '<div class="diagram-placeholder">\n <p>Diagram unavailable — rendering tools not found</p>\n <p>Install: <code>npm install -g @mermaid-js/mermaid-cli</code></p>\n</div>';
|
|
9
|
+
|
|
10
|
+
function stripSvgDimensions(svgStr) {
|
|
11
|
+
return svgStr
|
|
12
|
+
.replace(/<svg([^>]*)\s+width="[^"]*"/, '<svg$1')
|
|
13
|
+
.replace(/<svg([^>]*)\s+height="[^"]*"/, '<svg$1');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makePlaceholder() {
|
|
17
|
+
return PLACEHOLDER_HTML;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function tryMmdc(mmdContent) {
|
|
21
|
+
const ts = Date.now();
|
|
22
|
+
const tmpIn = path.join(os.tmpdir(), 'gsd-scan-' + ts + '.mmd');
|
|
23
|
+
const tmpOut = path.join(os.tmpdir(), 'gsd-scan-' + ts + '.svg');
|
|
24
|
+
try {
|
|
25
|
+
fs.writeFileSync(tmpIn, mmdContent, 'utf8');
|
|
26
|
+
execSync('mmdc -i "' + tmpIn + '" -o "' + tmpOut + '" -t dark --quiet', { timeout: 30000, stdio: 'pipe' });
|
|
27
|
+
const svg = fs.readFileSync(tmpOut, 'utf8');
|
|
28
|
+
return { svgContent: stripSvgDimensions(svg), rendered: true, rendererUsed: 'mermaid-cli' };
|
|
29
|
+
} catch { return null; }
|
|
30
|
+
finally {
|
|
31
|
+
try { fs.unlinkSync(tmpIn); } catch {}
|
|
32
|
+
try { fs.unlinkSync(tmpOut); } catch {}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function tryD2(mmdContent, type) {
|
|
37
|
+
if (type !== 'system-architecture' && type !== 'data-flow') return null;
|
|
38
|
+
const ts = Date.now();
|
|
39
|
+
const tmpIn = path.join(os.tmpdir(), 'gsd-scan-' + ts + '.d2');
|
|
40
|
+
const tmpOut = path.join(os.tmpdir(), 'gsd-scan-' + ts + '.svg');
|
|
41
|
+
try {
|
|
42
|
+
fs.writeFileSync(tmpIn, 'app -> db: query', 'utf8');
|
|
43
|
+
execSync('d2 "' + tmpIn + '" "' + tmpOut + '" --layout=dagre', { timeout: 30000, stdio: 'pipe' });
|
|
44
|
+
const svg = fs.readFileSync(tmpOut, 'utf8');
|
|
45
|
+
return { svgContent: stripSvgDimensions(svg), rendered: true, rendererUsed: 'd2' };
|
|
46
|
+
} catch { return null; }
|
|
47
|
+
finally {
|
|
48
|
+
try { fs.unlinkSync(tmpIn); } catch {}
|
|
49
|
+
try { fs.unlinkSync(tmpOut); } catch {}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function tryKroki(mmdContent) {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
const body = JSON.stringify({ diagram_source: mmdContent });
|
|
56
|
+
const host = process.env.KROKI_HOST || 'kroki.io';
|
|
57
|
+
const options = {
|
|
58
|
+
hostname: host, port: 443,
|
|
59
|
+
path: '/mermaid/svg', method: 'POST',
|
|
60
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
61
|
+
timeout: 15000
|
|
62
|
+
};
|
|
63
|
+
const req = https.request(options, (res) => {
|
|
64
|
+
let data = '';
|
|
65
|
+
res.on('data', c => { data += c; });
|
|
66
|
+
res.on('end', () => {
|
|
67
|
+
if (data.trimStart().startsWith('<svg')) {
|
|
68
|
+
resolve({ svgContent: stripSvgDimensions(data), rendered: true, rendererUsed: 'kroki' });
|
|
69
|
+
} else { resolve(null); }
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
req.on('error', () => resolve(null));
|
|
73
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
74
|
+
req.write(body);
|
|
75
|
+
req.end();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function renderDiagram(mmdContent, type, options) {
|
|
80
|
+
try {
|
|
81
|
+
const mmdc = tryMmdc(mmdContent);
|
|
82
|
+
if (mmdc) return mmdc;
|
|
83
|
+
const d2 = tryD2(mmdContent, type);
|
|
84
|
+
if (d2) return d2;
|
|
85
|
+
// tryKroki is async; skip in sync rendering path — Kroki available via async wrapper if needed
|
|
86
|
+
return { svgContent: makePlaceholder(), rendered: false, rendererUsed: 'placeholder' };
|
|
87
|
+
} catch {
|
|
88
|
+
return { svgContent: makePlaceholder(), rendered: false, rendererUsed: 'placeholder' };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { renderDiagram };
|