@tekyzinc/gsd-t 2.33.12 → 2.34.11
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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function esc(s) {
|
|
4
|
+
return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function buildMetricCards(d) {
|
|
8
|
+
d = d || {};
|
|
9
|
+
const loc = d.totalLoc > 999 ? (d.totalLoc / 1000).toFixed(1) + 'k' : (d.totalLoc || 0);
|
|
10
|
+
const cov = d.testCoverage || 'N/A';
|
|
11
|
+
const metrics = [
|
|
12
|
+
{ label: 'Files Scanned', value: d.filesScanned || 0, sub: 'across all components', bar: 'g' },
|
|
13
|
+
{ label: 'Lines of Code', value: loc, sub: 'source code', bar: '' },
|
|
14
|
+
{ label: 'Critical Issues', value: d.debtCritical || 0, sub: 'requires immediate fix', bar: 'r' },
|
|
15
|
+
{ label: 'High Issues', value: d.debtHigh || 0, sub: 'fix before next release', bar: 'o' },
|
|
16
|
+
{ label: 'Medium Issues', value: d.debtMedium || 0, sub: 'plan to address', bar: 'y' },
|
|
17
|
+
{ label: 'Test Coverage', value: cov, sub: 'passing tests', bar: 'g' }
|
|
18
|
+
];
|
|
19
|
+
const cards = metrics.map(m =>
|
|
20
|
+
'<div class="mc"><div class="mc-bar' + (m.bar ? ' ' + m.bar : '') + '"></div>' +
|
|
21
|
+
'<div class="mc-lbl">' + m.label + '</div>' +
|
|
22
|
+
'<div class="mc-val">' + m.value + '</div>' +
|
|
23
|
+
'<div class="mc-sub">' + m.sub + '</div></div>'
|
|
24
|
+
).join('');
|
|
25
|
+
return '<section id="summary"><div class="sl">Summary</div><div class="mxg">' + cards + '</div></section>';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildDomainHealth(d) {
|
|
29
|
+
d = d || {};
|
|
30
|
+
const domains = d.domains || [];
|
|
31
|
+
if (!domains.length) {
|
|
32
|
+
return '<section id="domains"><div class="sl">Component Inventory</div>' +
|
|
33
|
+
'<p style="color:var(--muted2);font-size:12px">No component data available.</p></section>';
|
|
34
|
+
}
|
|
35
|
+
const rows = domains.map(item => {
|
|
36
|
+
const bigFile = parseInt(item.size) > 500;
|
|
37
|
+
const sizeColor = bigFile ? 'color:var(--amber)' : 'color:var(--blue)';
|
|
38
|
+
return '<tr>' +
|
|
39
|
+
'<td><strong>' + esc(item.name) + '</strong></td>' +
|
|
40
|
+
'<td style="font-family:\'Consolas\',monospace;font-size:10px;color:var(--muted2)">' + esc(item.filePath || '') + '</td>' +
|
|
41
|
+
'<td class="loc-cell" style="' + sizeColor + '">' + esc(item.size || '') + '</td>' +
|
|
42
|
+
'<td style="color:var(--muted2);font-size:11px">' + esc(item.purpose || '') + '</td>' +
|
|
43
|
+
'</tr>';
|
|
44
|
+
}).join('');
|
|
45
|
+
return '<section id="domains"><div class="sl">Component Inventory</div>' +
|
|
46
|
+
'<div class="tw"><table><thead><tr><th>Component</th><th>File(s)</th><th>Lines</th><th>Purpose</th></tr></thead>' +
|
|
47
|
+
'<tbody>' + rows + '</tbody></table></div></section>';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildDiagramSection(d) {
|
|
51
|
+
const secId = 'diagram-' + d.type;
|
|
52
|
+
const cardTitle = esc(d.title + (d.typeBadge ? ' \u2014 ' + d.typeBadge : ''));
|
|
53
|
+
let diagramContent;
|
|
54
|
+
if (d.svgContent && !d.svgContent.includes('diagram-placeholder')) {
|
|
55
|
+
diagramContent = '<div style="width:100%;overflow:auto">' + d.svgContent + '</div>';
|
|
56
|
+
} else {
|
|
57
|
+
diagramContent = d.svgContent ||
|
|
58
|
+
'<div class="diagram-placeholder"><p>Diagram unavailable</p></div>';
|
|
59
|
+
}
|
|
60
|
+
return '<section id="' + secId + '" class="diagram-section">' +
|
|
61
|
+
'<div class="sl">' + esc(d.title) + '</div>' +
|
|
62
|
+
'<div class="dc" data-title="' + cardTitle + '">' +
|
|
63
|
+
'<div class="dc-h"><div class="dc-hl">' +
|
|
64
|
+
'<span class="dc-t">' + esc(d.title) + '</span>' +
|
|
65
|
+
'<span class="dc-tag">' + esc(d.typeBadge) + '</span>' +
|
|
66
|
+
'</div><button class="btn-exp" onclick="expandDiagram(this)">⛶<span class="lbl">Expand</span></button>' +
|
|
67
|
+
'</div>' +
|
|
68
|
+
'<div class="dc-b">' + diagramContent + '</div>' +
|
|
69
|
+
'<div class="dc-n">' + esc(d.note) + '</div>' +
|
|
70
|
+
'</div></section>';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildTechDebt(d) {
|
|
74
|
+
d = d || {};
|
|
75
|
+
const items = d.techDebt || [];
|
|
76
|
+
if (!items.length) {
|
|
77
|
+
return '<section id="tech-debt"><div class="sl">Tech Debt Register</div>' +
|
|
78
|
+
'<p style="color:var(--muted2);font-size:12px">No open tech debt items.</p></section>';
|
|
79
|
+
}
|
|
80
|
+
const sevClass = { critical: 'c', high: 'h', medium: 'm', low: 'l', info: 'i' };
|
|
81
|
+
const rows = items.map(i => {
|
|
82
|
+
const sc = sevClass[(i.severity || '').toLowerCase()] || 'm';
|
|
83
|
+
return '<tr>' +
|
|
84
|
+
'<td><span class="bx ' + sc + '">' + esc(i.severity || '') + '</span></td>' +
|
|
85
|
+
'<td style="font-family:\'Consolas\',monospace;font-size:11px;color:var(--blue)">' + esc(i.domain || '') + '</td>' +
|
|
86
|
+
'<td>' + esc(i.issue || '') + '</td>' +
|
|
87
|
+
'<td><code>' + esc(i.location || '') + '</code></td>' +
|
|
88
|
+
'<td style="color:var(--muted2);font-size:11px">' + esc(i.effort || '') + '</td>' +
|
|
89
|
+
'</tr>';
|
|
90
|
+
}).join('');
|
|
91
|
+
return '<section id="tech-debt"><div class="sl">Tech Debt Register</div>' +
|
|
92
|
+
'<div class="tw"><table><thead><tr><th>Severity</th><th>Domain</th><th>Issue</th><th>Location</th><th>Effort</th></tr></thead>' +
|
|
93
|
+
'<tbody>' + rows + '</tbody></table></div></section>';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const ICONS = {
|
|
97
|
+
security: '🛑', architecture: '⚡', reliability: '📊',
|
|
98
|
+
quality: '📄', performance: '⚠', strength: '✅', default: '📋'
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function buildFindings(d) {
|
|
102
|
+
d = d || {};
|
|
103
|
+
const findings = d.findings || [];
|
|
104
|
+
if (!findings.length) {
|
|
105
|
+
return '<section id="findings"><div class="sl">Key Findings</div>' +
|
|
106
|
+
'<p style="color:var(--muted2);font-size:12px">No findings recorded.</p></section>';
|
|
107
|
+
}
|
|
108
|
+
const cards = findings.map(f => {
|
|
109
|
+
const cat = (f.category || '').toLowerCase();
|
|
110
|
+
const ico = ICONS[cat] || ICONS.default;
|
|
111
|
+
const rec = f.recommendation
|
|
112
|
+
? ' <strong style="color:var(--text)">Fix:</strong> ' + esc(f.recommendation) : '';
|
|
113
|
+
return '<div class="fi"><div class="ico">' + ico + '</div>' +
|
|
114
|
+
'<div><h4>' + esc(f.title || '') + '</h4>' +
|
|
115
|
+
'<p>' + esc(f.description || '') + rec + '</p>' +
|
|
116
|
+
'</div></div>';
|
|
117
|
+
}).join('');
|
|
118
|
+
return '<section id="findings"><div class="sl">Key Findings</div><div class="fl">' + cards + '</div></section>';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { buildMetricCards, buildDomainHealth, buildDiagramSection, buildTechDebt, buildFindings };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
function buildCss() {
|
|
6
|
+
return `:root{--bg:#070b12;--surface:#0c1120;--card:#0f1624;--card2:#131c2e;--border:#1a2840;--border2:#243654;--blue:#3b82f6;--violet:#7c3aed;--cyan:#06b6d4;--green:#10b981;--amber:#f59e0b;--red:#ef4444;--orange:#f97316;--text:#e2e8f0;--muted:#4b6080;--muted2:#7a96b8;--radius:12px}
|
|
7
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html{scroll-behavior:smooth}
|
|
8
|
+
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);display:flex;min-height:100vh}
|
|
9
|
+
nav{width:192px;min-width:192px;background:var(--surface);border-right:1px solid var(--border);position:sticky;top:0;height:100vh;overflow-y:auto;padding:14px 0 24px;display:flex;flex-direction:column;gap:1px}
|
|
10
|
+
.nb{padding:0 12px 14px;border-bottom:1px solid var(--border);margin-bottom:6px}
|
|
11
|
+
.nb .pill{font-size:8.5px;font-weight:800;letter-spacing:1.5px;text-transform:uppercase;background:linear-gradient(135deg,var(--blue),var(--violet));color:#fff;padding:2px 7px;border-radius:4px;display:inline-block;margin-bottom:5px}
|
|
12
|
+
.nb h2{font-size:12.5px;font-weight:700}.nb p{font-size:10px;color:var(--muted2);margin-top:1px}
|
|
13
|
+
.ns{font-size:8.5px;font-weight:800;letter-spacing:1.5px;text-transform:uppercase;color:var(--muted);padding:10px 12px 3px;opacity:.55}
|
|
14
|
+
nav a{display:flex;align-items:center;gap:7px;padding:5px 12px;color:var(--muted2);text-decoration:none;font-size:11.5px;font-weight:500;border-left:2px solid transparent;transition:all .1s}
|
|
15
|
+
nav a:hover,nav a.active{color:var(--text);background:rgba(59,130,246,.07);border-left-color:var(--blue)}
|
|
16
|
+
.nd{width:5px;height:5px;border-radius:50%;background:var(--muted);flex-shrink:0;transition:background .1s}
|
|
17
|
+
nav a:hover .nd,nav a.active .nd{background:var(--blue)}.nd.g{background:var(--green)}.nd.y{background:var(--amber)}.nd.r{background:var(--red)}
|
|
18
|
+
main{flex:1;overflow-y:auto;padding:24px 36px 60px;min-width:0}
|
|
19
|
+
.ph{display:flex;align-items:baseline;justify-content:space-between;padding-bottom:14px;border-bottom:1px solid var(--border);margin-bottom:24px;flex-wrap:wrap;gap:6px}
|
|
20
|
+
.ph h1{font-size:15px;font-weight:700;display:flex;align-items:center;gap:8px}.ph h1 .bc{font-size:10px;font-weight:400;color:var(--muted2)}
|
|
21
|
+
.ph .meta{font-size:10.5px;color:var(--muted2);display:flex;gap:12px;flex-wrap:wrap}.ph .meta .sep{color:var(--border2)}
|
|
22
|
+
section{margin-bottom:44px;scroll-margin-top:20px}
|
|
23
|
+
.sl{font-size:9px;font-weight:800;letter-spacing:1.5px;text-transform:uppercase;color:var(--muted2);margin-bottom:12px;display:flex;align-items:center;gap:8px}
|
|
24
|
+
.sl::after{content:'';flex:1;height:1px;background:var(--border)}
|
|
25
|
+
.mxg{display:grid;grid-template-columns:repeat(auto-fit,minmax(138px,1fr));gap:10px}
|
|
26
|
+
.mc{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow:hidden;position:relative}
|
|
27
|
+
.mc-bar{height:2px;border-radius:1px;margin-bottom:10px;background:linear-gradient(90deg,var(--blue),var(--violet))}
|
|
28
|
+
.mc-bar.g{background:linear-gradient(90deg,var(--green),#34d399)}.mc-bar.y{background:linear-gradient(90deg,var(--amber),#fbbf24)}.mc-bar.r{background:linear-gradient(90deg,var(--red),#f87171)}.mc-bar.o{background:linear-gradient(90deg,var(--orange),#fb923c)}
|
|
29
|
+
.mc-lbl{font-size:9px;font-weight:800;letter-spacing:.7px;text-transform:uppercase;color:var(--muted2);margin-bottom:4px}.mc-val{font-size:24px;font-weight:800;color:var(--text);line-height:1}.mc-sub{font-size:9.5px;color:var(--muted2);margin-top:3px}
|
|
30
|
+
.dc{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;margin-bottom:16px;transition:border-color .15s}
|
|
31
|
+
.dc:hover{border-color:var(--border2)}.dc-h{display:flex;align-items:center;justify-content:space-between;padding:9px 14px;background:var(--card2);border-bottom:1px solid var(--border)}
|
|
32
|
+
.dc-hl{display:flex;align-items:center;gap:8px}.dc-t{font-size:11.5px;font-weight:700;color:var(--text)}
|
|
33
|
+
.dc-tag{font-size:8.5px;font-weight:800;letter-spacing:.8px;text-transform:uppercase;padding:2px 8px;border-radius:20px;background:rgba(59,130,246,.1);color:var(--blue);border:1px solid rgba(59,130,246,.2)}
|
|
34
|
+
.btn-exp{display:flex;align-items:center;gap:5px;background:rgba(255,255,255,.04);border:1px solid var(--border2);border-radius:6px;color:var(--muted2);cursor:pointer;padding:4px 10px;font-size:12px;transition:all .15s}
|
|
35
|
+
.btn-exp span.lbl{font-size:9px;font-weight:700;letter-spacing:.5px;text-transform:uppercase}.btn-exp:hover{background:rgba(59,130,246,.12);border-color:var(--blue);color:var(--blue)}
|
|
36
|
+
.dc-b{min-height:520px;display:flex;align-items:flex-start;justify-content:center;background:radial-gradient(ellipse 80% 60% at 50% 20%,rgba(59,130,246,.04) 0%,transparent 70%),var(--bg);padding:24px 20px;overflow:auto}
|
|
37
|
+
.dc-b .mermaid{width:100%;display:flex;align-items:flex-start;justify-content:center}.dc-b .mermaid svg{width:100%!important;height:auto!important;display:block}
|
|
38
|
+
.dc-b svg{width:100%;height:auto;display:block}
|
|
39
|
+
.diagram-placeholder{background:rgba(255,255,255,.03);border:1px dashed var(--border);border-radius:8px;padding:48px 32px;text-align:center;color:var(--muted2);font-size:13px;width:100%}
|
|
40
|
+
.diagram-placeholder code{font-family:monospace;background:rgba(255,255,255,.07);padding:2px 6px;border-radius:4px}
|
|
41
|
+
.dc-n{padding:9px 14px;font-size:11px;color:var(--muted2);border-top:1px solid var(--border);background:var(--card2);line-height:1.5}
|
|
42
|
+
.dc-n strong{color:var(--text)}.dc-n code{font-family:'Consolas',monospace;font-size:10px;background:rgba(255,255,255,.05);padding:1px 5px;border-radius:3px;color:#7dd3fc}
|
|
43
|
+
.tw{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
|
|
44
|
+
table{width:100%;border-collapse:collapse;font-size:12px}
|
|
45
|
+
thead th{background:var(--card2);color:var(--muted2);font-size:8.5px;font-weight:800;letter-spacing:1px;text-transform:uppercase;padding:9px 13px;text-align:left;border-bottom:1px solid var(--border)}
|
|
46
|
+
tbody td{padding:9px 13px;border-bottom:1px solid var(--border);vertical-align:top}
|
|
47
|
+
tbody tr:last-child td{border-bottom:none}tbody tr:hover{background:rgba(255,255,255,.012)}
|
|
48
|
+
td code{font-family:'Consolas',monospace;font-size:10px;background:rgba(255,255,255,.05);padding:1px 5px;border-radius:3px;color:#7dd3fc}
|
|
49
|
+
.loc-cell{font-family:'Consolas',monospace;font-size:11px;color:var(--blue);white-space:nowrap}
|
|
50
|
+
.bx{display:inline-flex;align-items:center;padding:2px 7px;border-radius:20px;font-size:8.5px;font-weight:800;letter-spacing:.4px;text-transform:uppercase}
|
|
51
|
+
.bx.c{background:rgba(239,68,68,.1);color:var(--red);border:1px solid rgba(239,68,68,.22)}.bx.h{background:rgba(249,115,22,.1);color:var(--orange);border:1px solid rgba(249,115,22,.22)}
|
|
52
|
+
.bx.m{background:rgba(245,158,11,.1);color:var(--amber);border:1px solid rgba(245,158,11,.22)}.bx.l{background:rgba(16,185,129,.1);color:var(--green);border:1px solid rgba(16,185,129,.22)}
|
|
53
|
+
.bx.i{background:rgba(59,130,246,.1);color:var(--blue);border:1px solid rgba(59,130,246,.22)}
|
|
54
|
+
.fl{display:flex;flex-direction:column;gap:8px}
|
|
55
|
+
.fi{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:13px 14px;display:flex;gap:11px;align-items:flex-start}
|
|
56
|
+
.fi .ico{font-size:15px;flex-shrink:0;margin-top:1px}.fi h4{font-size:11.5px;font-weight:700;margin-bottom:2px}.fi p{font-size:11px;color:var(--muted2)}
|
|
57
|
+
.fi code{font-family:'Consolas',monospace;font-size:10px;background:rgba(255,255,255,.05);padding:1px 5px;border-radius:3px;color:#7dd3fc}
|
|
58
|
+
#modal{display:none;position:fixed;inset:0;background:rgba(4,7,14,.97);z-index:1000;flex-direction:column}#modal.open{display:flex}
|
|
59
|
+
.mbar{display:flex;align-items:center;justify-content:space-between;padding:10px 18px;background:rgba(12,17,32,.98);border-bottom:1px solid var(--border);backdrop-filter:blur(8px);flex-shrink:0}
|
|
60
|
+
.mbar h3{font-size:12.5px;font-weight:700}.mbar .hint{font-size:9.5px;color:var(--muted2);margin-left:10px}.mbar-r{display:flex;align-items:center;gap:8px}
|
|
61
|
+
.btn-z{background:rgba(255,255,255,.05);border:1px solid var(--border2);border-radius:6px;color:var(--muted2);cursor:pointer;padding:4px 10px;font-size:13px;font-weight:700;transition:all .12s}
|
|
62
|
+
.btn-z:hover{background:rgba(59,130,246,.12);border-color:var(--blue);color:var(--blue)}.zlbl{font-size:10.5px;color:var(--muted2);min-width:36px;text-align:center}
|
|
63
|
+
.btn-cls{background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.2);border-radius:7px;color:#f87171;cursor:pointer;padding:5px 14px;font-size:12px;font-weight:600;transition:all .12s}
|
|
64
|
+
.btn-cls:hover{background:rgba(239,68,68,.2);border-color:var(--red)}
|
|
65
|
+
#mcontent{flex:1;overflow:auto;display:flex;align-items:center;justify-content:center;padding:48px}
|
|
66
|
+
#mdiagram{transform-origin:center center;transition:transform .12s}#mdiagram svg{display:block;width:auto;max-width:100%;height:auto}
|
|
67
|
+
footer{margin-top:48px;padding-top:14px;border-top:1px solid var(--border);font-size:9.5px;color:var(--muted);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:5px;opacity:.5}
|
|
68
|
+
footer a{color:var(--muted2);text-decoration:none}footer a:hover{color:var(--blue)}
|
|
69
|
+
::-webkit-scrollbar{width:4px;height:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border2);border-radius:4px}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildScript() {
|
|
73
|
+
return `<script>
|
|
74
|
+
let zoom=1;
|
|
75
|
+
function expandDiagram(btn){const card=btn.closest('.dc');const title=card.dataset.title||'Diagram';const svg=card.querySelector('.dc-b svg');if(!svg)return;const clone=svg.cloneNode(true);clone.style.cssText='display:block;width:auto;height:auto;max-width:90vw;max-height:85vh;';clone.removeAttribute('width');clone.removeAttribute('height');const wrap=document.getElementById('mdiagram');wrap.innerHTML='';wrap.appendChild(clone);document.getElementById('mtitle').textContent=title;zoom=1;applyZoom();document.getElementById('modal').classList.add('open');document.body.style.overflow='hidden';}
|
|
76
|
+
function closeModal(){document.getElementById('modal').classList.remove('open');document.body.style.overflow='';}
|
|
77
|
+
function adjustZoom(d){zoom=Math.max(0.2,Math.min(5,zoom+d));applyZoom();}
|
|
78
|
+
function resetZoom(){zoom=1;applyZoom();}
|
|
79
|
+
function applyZoom(){document.getElementById('mdiagram').style.transform='scale('+zoom+')';document.getElementById('zlbl').textContent=Math.round(zoom*100)+'%';}
|
|
80
|
+
document.getElementById('mcontent').addEventListener('wheel',e=>{e.preventDefault();adjustZoom(e.deltaY<0?0.06:-0.06);},{passive:false});
|
|
81
|
+
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeModal();});
|
|
82
|
+
document.getElementById('modal').addEventListener('click',e=>{if(e.target===document.getElementById('modal')||e.target===document.getElementById('mcontent'))closeModal();});
|
|
83
|
+
const navLinks=document.querySelectorAll('nav a[href^="#"]');document.querySelectorAll('section[id]').forEach(s=>new IntersectionObserver(entries=>{if(entries[0].isIntersecting){navLinks.forEach(l=>l.classList.remove('active'));const a=document.querySelector('nav a[href="#'+entries[0].target.id+'"]');if(a)a.classList.add('active');}},{threshold:0.15}).observe(s));
|
|
84
|
+
<\/script>`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildSidebar(projectName, version, scanDate, diagTypes) {
|
|
88
|
+
const vp = (version ? esc(version) + ' · ' : '') + esc(scanDate || '');
|
|
89
|
+
const ALL = [['system-architecture','System Architecture'],['app-architecture','App Architecture'],['workflow','Workflow'],['data-flow','Data Flow'],['sequence','Sequence'],['database-schema','Database Schema']];
|
|
90
|
+
const dlinks = ALL.filter(([t]) => !diagTypes || diagTypes.includes(t)).map(([t, l]) => '<a href="#diagram-' + t + '"><span class="nd"></span>' + esc(l) + '</a>').join('\n ');
|
|
91
|
+
return '<nav>\n <div class="nb"><div class="pill">GSD‑T Scan</div><h2>' + esc(projectName) + '</h2><p>' + vp + '</p></div>\n <div class="ns">Overview</div>\n <a href="#summary"><span class="nd g"></span>Summary</a>\n <a href="#domains"><span class="nd"></span>Domains</a>\n <div class="ns">Diagrams</div>\n ' + dlinks + '\n <div class="ns">Analysis</div>\n <a href="#tech-debt"><span class="nd y"></span>Tech Debt</a>\n <a href="#findings"><span class="nd r"></span>Key Findings</a>\n</nav>';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildPageHeader(data, opts) {
|
|
95
|
+
const projectName = data.projectName || path.basename(opts.projectRoot || process.cwd());
|
|
96
|
+
const stack = data.techStack || '';
|
|
97
|
+
const files = data.filesScanned ? data.filesScanned + ' files' : '';
|
|
98
|
+
const loc = data.totalLoc ? (data.totalLoc > 999 ? (data.totalLoc / 1000).toFixed(1) + 'k' : data.totalLoc) + ' LoC' : '';
|
|
99
|
+
const fileLoc = [files, loc].filter(Boolean).join(' \u00b7 ');
|
|
100
|
+
const metaParts = [stack, fileLoc, data.scanDate || ''].filter(Boolean);
|
|
101
|
+
const metaHtml = metaParts.map((m, i) =>
|
|
102
|
+
(i > 0 ? '<span class="sep">|</span>' : '') + '<span>' + esc(m) + '</span>'
|
|
103
|
+
).join('');
|
|
104
|
+
return '<div class="ph"><h1><span class="bc">GSD‑T › Scan ›</span> ' +
|
|
105
|
+
esc(projectName) + ' Codebase Report</h1><div class="meta">' + metaHtml + '</div></div>';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildHtmlSkeleton(title, css, sidebar, body) {
|
|
109
|
+
return `<!DOCTYPE html>
|
|
110
|
+
<html lang="en">
|
|
111
|
+
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
112
|
+
<title>${title}</title>
|
|
113
|
+
<style>
|
|
114
|
+
${css}
|
|
115
|
+
</style></head>
|
|
116
|
+
<body>
|
|
117
|
+
${sidebar}
|
|
118
|
+
<div id="modal">
|
|
119
|
+
<div class="mbar">
|
|
120
|
+
<div style="display:flex;align-items:center;gap:8px"><h3 id="mtitle">Diagram</h3><span class="hint">Scroll to zoom · Esc to close</span></div>
|
|
121
|
+
<div class="mbar-r">
|
|
122
|
+
<button class="btn-z" onclick="adjustZoom(-0.15)">−</button>
|
|
123
|
+
<span class="zlbl" id="zlbl">100%</span>
|
|
124
|
+
<button class="btn-z" onclick="adjustZoom(0.15)">+</button>
|
|
125
|
+
<button class="btn-z" onclick="resetZoom()" style="font-size:10px;padding:4px 9px">Reset</button>
|
|
126
|
+
<button class="btn-cls" onclick="closeModal()">✕ Close</button>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<div id="mcontent"><div id="mdiagram"></div></div>
|
|
130
|
+
</div>
|
|
131
|
+
<main>
|
|
132
|
+
${body}
|
|
133
|
+
<footer><span>GSD-T Scan Report</span><span><a href="https://mermaid.js.org">Mermaid.js (MIT)</a></span></footer>
|
|
134
|
+
</main>
|
|
135
|
+
${buildScript()}
|
|
136
|
+
</body>
|
|
137
|
+
</html>`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function esc(s) { return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
141
|
+
const sections = require('./scan-report-sections.js');
|
|
142
|
+
function generateReport(analysisData, schemaData, diagrams, options) {
|
|
143
|
+
try {
|
|
144
|
+
const safeData = analysisData || {};
|
|
145
|
+
const safeDiagrams = (Array.isArray(diagrams) ? diagrams : []).filter(d => d && d.type);
|
|
146
|
+
const opts = options || {};
|
|
147
|
+
const projectName = safeData.projectName || path.basename(opts.projectRoot || process.cwd());
|
|
148
|
+
const css = buildCss();
|
|
149
|
+
const sidebar = buildSidebar(projectName, safeData.version || '', safeData.scanDate || '', safeDiagrams.map(d => d.type));
|
|
150
|
+
const pageHeader = buildPageHeader(safeData, opts);
|
|
151
|
+
const body = pageHeader
|
|
152
|
+
+ sections.buildMetricCards(safeData)
|
|
153
|
+
+ sections.buildDomainHealth(safeData)
|
|
154
|
+
+ safeDiagrams.map(sections.buildDiagramSection).join('')
|
|
155
|
+
+ sections.buildTechDebt(safeData)
|
|
156
|
+
+ sections.buildFindings(safeData);
|
|
157
|
+
const html = buildHtmlSkeleton(esc(projectName) + ' \u2014 GSD-T Scan Report', css, sidebar, body);
|
|
158
|
+
const outputPath = path.join(opts.outputDir || opts.projectRoot || process.cwd(), 'scan-report.html');
|
|
159
|
+
fs.writeFileSync(outputPath, html, 'utf8');
|
|
160
|
+
return {
|
|
161
|
+
outputPath,
|
|
162
|
+
diagramsRendered: safeDiagrams.filter(d => d.rendered).length,
|
|
163
|
+
diagramsPlaceholder: safeDiagrams.filter(d => !d.rendered).length
|
|
164
|
+
};
|
|
165
|
+
} catch (err) {
|
|
166
|
+
process.stderr.write('scan-report error: ' + err.message + '\n');
|
|
167
|
+
return { outputPath: null, error: err.message };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
generateReport,
|
|
173
|
+
buildCss,
|
|
174
|
+
buildSidebar,
|
|
175
|
+
buildHtmlSkeleton,
|
|
176
|
+
buildMetricCards: sections.buildMetricCards,
|
|
177
|
+
buildDomainHealth: sections.buildDomainHealth,
|
|
178
|
+
buildDiagramSection: sections.buildDiagramSection,
|
|
179
|
+
buildTechDebt: sections.buildTechDebt,
|
|
180
|
+
buildFindings: sections.buildFindings
|
|
181
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
function parsePrisma(filePath, warnings) {
|
|
6
|
+
try {
|
|
7
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
8
|
+
const entities = [];
|
|
9
|
+
const modelRe = /model\s+(\w+)\s*\{([^}]+)\}/g;
|
|
10
|
+
let m;
|
|
11
|
+
while ((m = modelRe.exec(content)) !== null) {
|
|
12
|
+
const name = m[1];
|
|
13
|
+
const body = m[2];
|
|
14
|
+
const fields = [];
|
|
15
|
+
let primaryKey = null;
|
|
16
|
+
const relations = [];
|
|
17
|
+
for (const line of body.split('\n')) {
|
|
18
|
+
const trimmed = line.trim();
|
|
19
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) continue;
|
|
20
|
+
const parts = trimmed.split(/\s+/);
|
|
21
|
+
if (parts.length < 2) continue;
|
|
22
|
+
const fieldName = parts[0];
|
|
23
|
+
const rawType = parts[1];
|
|
24
|
+
const type = rawType.replace('?', '').replace('[]', '');
|
|
25
|
+
const nullable = rawType.includes('?');
|
|
26
|
+
const unique = trimmed.includes('@unique');
|
|
27
|
+
if (trimmed.includes('@id')) primaryKey = fieldName;
|
|
28
|
+
if (trimmed.includes('@relation')) {
|
|
29
|
+
const relType = rawType.includes('[]') ? 'one-to-many' : 'many-to-one';
|
|
30
|
+
relations.push({ type: relType, fromEntity: name, toEntity: type, throughTable: null });
|
|
31
|
+
} else {
|
|
32
|
+
fields.push({ name: fieldName, type, nullable, unique });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
entities.push({ name, fields, primaryKey, relations });
|
|
36
|
+
}
|
|
37
|
+
return entities;
|
|
38
|
+
} catch (e) { warnings.push('parsePrisma: ' + e.message); return []; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseTypeOrm(files, warnings) {
|
|
42
|
+
const entities = [];
|
|
43
|
+
for (const filePath of files) {
|
|
44
|
+
try {
|
|
45
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
46
|
+
const classMatch = content.match(/export class (\w+)/);
|
|
47
|
+
if (!classMatch) continue;
|
|
48
|
+
const name = classMatch[1];
|
|
49
|
+
const fields = [];
|
|
50
|
+
let primaryKey = null;
|
|
51
|
+
const relations = [];
|
|
52
|
+
const lines = content.split('\n');
|
|
53
|
+
for (let i = 0; i < lines.length; i++) {
|
|
54
|
+
const line = lines[i].trim();
|
|
55
|
+
if (/@PrimaryGeneratedColumn|@PrimaryColumn/.test(line)) {
|
|
56
|
+
const next = (lines[i + 1] || '').trim();
|
|
57
|
+
const nm = next.match(/(\w+)\s*:/);
|
|
58
|
+
if (nm) primaryKey = nm[1];
|
|
59
|
+
} else if (/@Column/.test(line)) {
|
|
60
|
+
const next = (lines[i + 1] || '').trim();
|
|
61
|
+
const nm = next.match(/(\w+)\s*:\s*(\w+)/);
|
|
62
|
+
if (nm) fields.push({ name: nm[1], type: nm[2], nullable: line.includes('nullable: true'), unique: line.includes('unique: true') });
|
|
63
|
+
} else if (/@(ManyToOne|OneToMany|ManyToMany|OneToOne)\(/.test(line)) {
|
|
64
|
+
const tm = line.match(/\(\s*\(\s*\)\s*=>\s*(\w+)/);
|
|
65
|
+
const relMap = { ManyToOne: 'many-to-one', OneToMany: 'one-to-many', ManyToMany: 'many-to-many', OneToOne: 'one-to-one' };
|
|
66
|
+
const rm = line.match(/@(\w+)\(/);
|
|
67
|
+
if (tm && rm) relations.push({ type: relMap[rm[1]] || 'many-to-one', fromEntity: name, toEntity: tm[1], throughTable: null });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
entities.push({ name, fields, primaryKey, relations });
|
|
71
|
+
} catch (e) { warnings.push('parseTypeOrm: ' + e.message); }
|
|
72
|
+
}
|
|
73
|
+
return entities;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseDrizzle(files, warnings) {
|
|
77
|
+
const entities = [];
|
|
78
|
+
for (const filePath of files) {
|
|
79
|
+
try {
|
|
80
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
81
|
+
const tableRe = /(?:pgTable|mysqlTable|sqliteTable)\s*\(\s*['"](\w+)['"]/g;
|
|
82
|
+
let m;
|
|
83
|
+
while ((m = tableRe.exec(content)) !== null) {
|
|
84
|
+
const name = m[1];
|
|
85
|
+
const fields = [];
|
|
86
|
+
const colRe = /(\w+)\s*:\s*\w+\(/g;
|
|
87
|
+
let cm;
|
|
88
|
+
while ((cm = colRe.exec(content)) !== null) {
|
|
89
|
+
fields.push({ name: cm[1], type: 'unknown', nullable: true, unique: false });
|
|
90
|
+
}
|
|
91
|
+
entities.push({ name, fields, primaryKey: null, relations: [] });
|
|
92
|
+
}
|
|
93
|
+
} catch (e) { warnings.push('parseDrizzle: ' + e.message); }
|
|
94
|
+
}
|
|
95
|
+
return entities;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseMongoose(files, warnings) {
|
|
99
|
+
const entities = [];
|
|
100
|
+
for (const filePath of files) {
|
|
101
|
+
try {
|
|
102
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
103
|
+
const nameMatch = content.match(/const\s+(\w+)Schema\s*=\s*new\s+(?:mongoose\.)?Schema/);
|
|
104
|
+
const name = nameMatch ? nameMatch[1] : path.basename(filePath, path.extname(filePath));
|
|
105
|
+
const fields = [];
|
|
106
|
+
const relations = [];
|
|
107
|
+
const fieldRe = /(\w+)\s*:\s*\{[^}]*type\s*:\s*(\w+)/g;
|
|
108
|
+
let m;
|
|
109
|
+
while ((m = fieldRe.exec(content)) !== null) {
|
|
110
|
+
fields.push({ name: m[1], type: m[2], nullable: true, unique: false });
|
|
111
|
+
}
|
|
112
|
+
const refRe = /ref\s*:\s*['"](\w+)['"]/g;
|
|
113
|
+
while ((m = refRe.exec(content)) !== null) {
|
|
114
|
+
relations.push({ type: 'many-to-one', fromEntity: name, toEntity: m[1], throughTable: null });
|
|
115
|
+
}
|
|
116
|
+
entities.push({ name, fields, primaryKey: '_id', relations });
|
|
117
|
+
} catch (e) { warnings.push('parseMongoose: ' + e.message); }
|
|
118
|
+
}
|
|
119
|
+
return entities;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseSequelize(files, warnings) {
|
|
123
|
+
const entities = [];
|
|
124
|
+
for (const filePath of files) {
|
|
125
|
+
try {
|
|
126
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
127
|
+
const classMatch = content.match(/class (\w+) extends Model/);
|
|
128
|
+
if (!classMatch) continue;
|
|
129
|
+
const name = classMatch[1];
|
|
130
|
+
const fields = [];
|
|
131
|
+
let primaryKey = null;
|
|
132
|
+
const colRe = /(\w+)\s*:\s*\{[^}]*type\s*:\s*DataTypes\.(\w+)([^}]*)\}/g;
|
|
133
|
+
let m;
|
|
134
|
+
while ((m = colRe.exec(content)) !== null) {
|
|
135
|
+
const isPk = m[3].includes('primaryKey: true');
|
|
136
|
+
if (isPk) primaryKey = m[1];
|
|
137
|
+
fields.push({ name: m[1], type: 'DataTypes.' + m[2], nullable: !m[3].includes('allowNull: false'), unique: m[3].includes('unique: true') });
|
|
138
|
+
}
|
|
139
|
+
entities.push({ name, fields, primaryKey, relations: [] });
|
|
140
|
+
} catch (e) { warnings.push('parseSequelize: ' + e.message); }
|
|
141
|
+
}
|
|
142
|
+
return entities;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseSqlAlchemy(files, warnings) {
|
|
146
|
+
const entities = [];
|
|
147
|
+
for (const filePath of files) {
|
|
148
|
+
try {
|
|
149
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
150
|
+
const classRe = /class (\w+)\s*\([^)]*Base[^)]*\):/g;
|
|
151
|
+
let m;
|
|
152
|
+
while ((m = classRe.exec(content)) !== null) {
|
|
153
|
+
const name = m[1];
|
|
154
|
+
const fields = [];
|
|
155
|
+
let primaryKey = null;
|
|
156
|
+
const colRe = /(\w+)\s*=\s*Column\((\w+)([^)]*)\)/g;
|
|
157
|
+
let cm;
|
|
158
|
+
while ((cm = colRe.exec(content)) !== null) {
|
|
159
|
+
if (cm[3].includes('primary_key=True')) primaryKey = cm[1];
|
|
160
|
+
fields.push({ name: cm[1], type: cm[2], nullable: !cm[3].includes('nullable=False'), unique: cm[3].includes('unique=True') });
|
|
161
|
+
}
|
|
162
|
+
entities.push({ name, fields, primaryKey, relations: [] });
|
|
163
|
+
}
|
|
164
|
+
} catch (e) { warnings.push('parseSqlAlchemy: ' + e.message); }
|
|
165
|
+
}
|
|
166
|
+
return entities;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function parseRawSql(files, warnings) {
|
|
170
|
+
const entities = [];
|
|
171
|
+
for (const filePath of files) {
|
|
172
|
+
try {
|
|
173
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
174
|
+
const tableRe = /CREATE TABLE\s+[`"]?(\w+)[`"]?\s*\(([^;]+)\)/gi;
|
|
175
|
+
let m;
|
|
176
|
+
while ((m = tableRe.exec(content)) !== null) {
|
|
177
|
+
const name = m[1];
|
|
178
|
+
const body = m[2];
|
|
179
|
+
const fields = [];
|
|
180
|
+
let primaryKey = null;
|
|
181
|
+
for (const line of body.split('\n')) {
|
|
182
|
+
const trimmed = line.trim().replace(/,$/, '');
|
|
183
|
+
if (!trimmed || trimmed.startsWith('--') || /^PRIMARY KEY\s*\(/i.test(trimmed)) continue;
|
|
184
|
+
const parts = trimmed.split(/\s+/);
|
|
185
|
+
if (parts.length < 2) continue;
|
|
186
|
+
const fieldName = parts[0].replace(/[`"]/g, '');
|
|
187
|
+
const type = parts[1];
|
|
188
|
+
const isPk = /PRIMARY KEY/i.test(trimmed);
|
|
189
|
+
if (isPk) primaryKey = fieldName;
|
|
190
|
+
fields.push({ name: fieldName, type, nullable: !/NOT NULL/i.test(trimmed), unique: /\bUNIQUE\b/i.test(trimmed) });
|
|
191
|
+
}
|
|
192
|
+
entities.push({ name, fields, primaryKey, relations: [] });
|
|
193
|
+
}
|
|
194
|
+
} catch (e) { warnings.push('parseRawSql: ' + e.message); }
|
|
195
|
+
}
|
|
196
|
+
return entities;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = { parsePrisma, parseTypeOrm, parseDrizzle, parseMongoose, parseSequelize, parseSqlAlchemy, parseRawSql };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const parsers = require('./scan-schema-parsers');
|
|
5
|
+
|
|
6
|
+
function findFiles(dir, suffix) {
|
|
7
|
+
try {
|
|
8
|
+
const results = [];
|
|
9
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
10
|
+
for (const e of entries) {
|
|
11
|
+
const full = path.join(dir, e.name);
|
|
12
|
+
if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules') {
|
|
13
|
+
results.push(...findFiles(full, suffix));
|
|
14
|
+
} else if (e.isFile() && e.name.endsWith(suffix)) {
|
|
15
|
+
results.push(full);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return results;
|
|
19
|
+
} catch { return []; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function fileContains(filePath, substring) {
|
|
23
|
+
try { return fs.readFileSync(filePath, 'utf8').includes(substring); }
|
|
24
|
+
catch { return false; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function detectOrm(projectRoot) {
|
|
28
|
+
try {
|
|
29
|
+
const prisma = path.join(projectRoot, 'prisma', 'schema.prisma');
|
|
30
|
+
if (fs.existsSync(prisma)) return { ormType: 'prisma', files: [prisma] };
|
|
31
|
+
|
|
32
|
+
const entityFiles = findFiles(projectRoot, '.entity.ts').filter(f => fileContains(f, '@Entity'));
|
|
33
|
+
if (entityFiles.length) return { ormType: 'typeorm', files: entityFiles };
|
|
34
|
+
|
|
35
|
+
const drizzleFiles = findFiles(projectRoot, '.ts').filter(f => f.endsWith('schema.ts') && fileContains(f, 'drizzle-orm'));
|
|
36
|
+
if (drizzleFiles.length) return { ormType: 'drizzle', files: drizzleFiles };
|
|
37
|
+
|
|
38
|
+
const mongooseFiles = findFiles(projectRoot, '.ts').filter(f => fileContains(f, 'mongoose.Schema'));
|
|
39
|
+
if (mongooseFiles.length) return { ormType: 'mongoose', files: mongooseFiles };
|
|
40
|
+
|
|
41
|
+
const seqFiles = findFiles(projectRoot, '.ts').filter(f => fileContains(f, 'DataTypes') && fileContains(f, 'Model.init'));
|
|
42
|
+
if (seqFiles.length) return { ormType: 'sequelize', files: seqFiles };
|
|
43
|
+
|
|
44
|
+
const pyFiles = findFiles(projectRoot, '.py').filter(f => fileContains(f, 'declarative_base'));
|
|
45
|
+
if (pyFiles.length) return { ormType: 'sqlalchemy', files: pyFiles };
|
|
46
|
+
|
|
47
|
+
const sqlFiles = findFiles(projectRoot, '.sql').filter(f => fileContains(f, 'CREATE TABLE'));
|
|
48
|
+
if (sqlFiles.length) return { ormType: 'raw-sql', files: sqlFiles };
|
|
49
|
+
|
|
50
|
+
// Vector and document stores — check package.json and requirements.txt
|
|
51
|
+
try {
|
|
52
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
53
|
+
if (fs.existsSync(pkgPath)) {
|
|
54
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
55
|
+
const deps = Object.keys({ ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }).join(' ');
|
|
56
|
+
const VECTOR = ['@pinecone-database/pinecone', 'weaviate-client', '@weaviate/client', '@qdrant/js-client-rest', 'chromadb', '@zilliz/milvus2-sdk-node', 'hnswlib-node'];
|
|
57
|
+
const DOCDB = ['mongodb', 'couchdb', 'pouchdb', 'arangojs', 'couchbase', 'cassandra-driver'];
|
|
58
|
+
if (VECTOR.some(p => deps.includes(p))) return { ormType: 'vector-db', files: [pkgPath] };
|
|
59
|
+
if (DOCDB.some(p => deps.includes(p))) return { ormType: 'document-db', files: [pkgPath] };
|
|
60
|
+
}
|
|
61
|
+
const reqPath = path.join(projectRoot, 'requirements.txt');
|
|
62
|
+
if (fs.existsSync(reqPath)) {
|
|
63
|
+
const req = fs.readFileSync(reqPath, 'utf8');
|
|
64
|
+
if (/pinecone|weaviate|qdrant|chromadb|milvus|faiss|annoy/i.test(req)) return { ormType: 'vector-db', files: [reqPath] };
|
|
65
|
+
if (/pymongo|motor|couchdb|cassandra|arangodb/i.test(req)) return { ormType: 'document-db', files: [reqPath] };
|
|
66
|
+
}
|
|
67
|
+
} catch {}
|
|
68
|
+
|
|
69
|
+
return { ormType: null, files: [] };
|
|
70
|
+
} catch { return { ormType: null, files: [] }; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function extractSchema(projectRoot) {
|
|
74
|
+
try {
|
|
75
|
+
const { ormType, files } = detectOrm(projectRoot);
|
|
76
|
+
if (!ormType) return { detected: false, ormType: null, entities: [], parseWarnings: [] };
|
|
77
|
+
|
|
78
|
+
const warnings = [];
|
|
79
|
+
let entities = [];
|
|
80
|
+
|
|
81
|
+
if (ormType === 'prisma') entities = parsers.parsePrisma(files[0], warnings);
|
|
82
|
+
else if (ormType === 'typeorm') entities = parsers.parseTypeOrm(files, warnings);
|
|
83
|
+
else if (ormType === 'drizzle') entities = parsers.parseDrizzle(files, warnings);
|
|
84
|
+
else if (ormType === 'mongoose') entities = parsers.parseMongoose(files, warnings);
|
|
85
|
+
else if (ormType === 'sequelize') entities = parsers.parseSequelize(files, warnings);
|
|
86
|
+
else if (ormType === 'sqlalchemy') entities = parsers.parseSqlAlchemy(files, warnings);
|
|
87
|
+
else if (ormType === 'raw-sql') entities = parsers.parseRawSql(files, warnings);
|
|
88
|
+
else if (ormType === 'vector-db') entities = [
|
|
89
|
+
{ name: 'VectorIndex', fields: [{ name: 'id', type: 'string' }, { name: 'embedding', type: 'float[]' }, { name: 'metadata', type: 'object' }, { name: 'score', type: 'float' }], relations: [] },
|
|
90
|
+
{ name: 'Namespace', fields: [{ name: 'name', type: 'string' }, { name: 'vectorCount', type: 'int' }], relations: [{ fromEntity: 'Namespace', toEntity: 'VectorIndex', type: 'one-to-many' }] }
|
|
91
|
+
];
|
|
92
|
+
else if (ormType === 'document-db') entities = [
|
|
93
|
+
{ name: 'Collection', fields: [{ name: '_id', type: 'ObjectId' }, { name: 'data', type: 'object' }, { name: 'createdAt', type: 'date' }, { name: 'updatedAt', type: 'date' }], relations: [] }
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
entities = entities.filter(e => e.name && e.name.trim());
|
|
97
|
+
return { detected: true, ormType, entities, parseWarnings: warnings };
|
|
98
|
+
} catch (err) {
|
|
99
|
+
return { detected: false, ormType: null, entities: [], parseWarnings: ['Fatal: ' + err.message] };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { extractSchema };
|
package/commands/gsd-t-impact.md
CHANGED
|
@@ -45,7 +45,39 @@ grep -r "require.*{module}" src/
|
|
|
45
45
|
- Check `.gsd-t/contracts/schema-contract.md` — does this change data shape?
|
|
46
46
|
- Check `.gsd-t/contracts/component-contract.md` — does this change props/interface?
|
|
47
47
|
|
|
48
|
-
### D)
|
|
48
|
+
### D) New Consumer Analysis
|
|
49
|
+
|
|
50
|
+
**Trigger**: Run this section when the planned change adds a new client surface (web app, mobile app, CLI, external API, admin panel, etc.) that will consume the existing backend.
|
|
51
|
+
|
|
52
|
+
If no new consumer surface is being added, skip this section.
|
|
53
|
+
|
|
54
|
+
**Step 1 — List what the new consumer needs:**
|
|
55
|
+
```
|
|
56
|
+
Operations the new consumer requires:
|
|
57
|
+
- {operation-name}: {brief description}
|
|
58
|
+
- {operation-name}: {brief description}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Step 2 — Compare against existing backend operations:**
|
|
62
|
+
```bash
|
|
63
|
+
# Search for existing implementations of each needed operation
|
|
64
|
+
grep -r "{operation-name}" src/
|
|
65
|
+
grep -r "{operation-name}" commands/
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Step 3 — Classify each needed operation:**
|
|
69
|
+
|
|
70
|
+
| Operation | Classification | Action |
|
|
71
|
+
|-----------|---------------|--------|
|
|
72
|
+
| {op} | REUSE — identical operation exists | Call existing endpoint/function |
|
|
73
|
+
| {op} | EXTEND — similar operation exists, needs a variant | Add param or thin adapter |
|
|
74
|
+
| {op} | DUPLICATE — new endpoint would replicate existing logic | 🔴 Must route through shared layer |
|
|
75
|
+
| {op} | NEW — no equivalent exists | Build new, consider SharedCore placement |
|
|
76
|
+
|
|
77
|
+
**DUPLICATE items become 🔴 Breaking Changes** in Step 4 — they block execution until the shared layer is designed. Add them to the impact report under "Breaking Changes" with:
|
|
78
|
+
- Required Action: "Extract `{operation}` to shared-core domain; {new-consumer} and {existing-consumer} both call it from there."
|
|
79
|
+
|
|
80
|
+
### E) Test Coverage
|
|
49
81
|
- Which tests cover this code?
|
|
50
82
|
- Will they still pass after changes?
|
|
51
83
|
- Are there tests that assert current behavior that will become wrong?
|