mstro-app 0.3.8 → 0.4.0
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/LICENSE +191 -21
- package/PRIVACY.md +286 -62
- package/README.md +81 -58
- package/bin/commands/status.js +1 -1
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +22 -12
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/headless-logger.d.ts +10 -0
- package/dist/server/cli/headless/headless-logger.d.ts.map +1 -0
- package/dist/server/cli/headless/headless-logger.js +66 -0
- package/dist/server/cli/headless/headless-logger.js.map +1 -0
- package/dist/server/cli/headless/mcp-config.d.ts.map +1 -1
- package/dist/server/cli/headless/mcp-config.js +6 -5
- package/dist/server/cli/headless/mcp-config.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +4 -0
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/stall-assessor.d.ts +21 -0
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +100 -24
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +0 -12
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +22 -9
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts +8 -1
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +16 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +94 -11
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/mcp/bouncer-cli.d.ts +3 -0
- package/dist/server/mcp/bouncer-cli.d.ts.map +1 -0
- package/dist/server/mcp/bouncer-cli.js +54 -0
- package/dist/server/mcp/bouncer-cli.js.map +1 -0
- package/dist/server/services/plan/composer.d.ts +4 -0
- package/dist/server/services/plan/composer.d.ts.map +1 -0
- package/dist/server/services/plan/composer.js +181 -0
- package/dist/server/services/plan/composer.js.map +1 -0
- package/dist/server/services/plan/dependency-resolver.d.ts +28 -0
- package/dist/server/services/plan/dependency-resolver.d.ts.map +1 -0
- package/dist/server/services/plan/dependency-resolver.js +154 -0
- package/dist/server/services/plan/dependency-resolver.js.map +1 -0
- package/dist/server/services/plan/executor.d.ts +110 -0
- package/dist/server/services/plan/executor.d.ts.map +1 -0
- package/dist/server/services/plan/executor.js +641 -0
- package/dist/server/services/plan/executor.js.map +1 -0
- package/dist/server/services/plan/parser.d.ts +11 -0
- package/dist/server/services/plan/parser.d.ts.map +1 -0
- package/dist/server/services/plan/parser.js +445 -0
- package/dist/server/services/plan/parser.js.map +1 -0
- package/dist/server/services/plan/state-reconciler.d.ts +2 -0
- package/dist/server/services/plan/state-reconciler.d.ts.map +1 -0
- package/dist/server/services/plan/state-reconciler.js +145 -0
- package/dist/server/services/plan/state-reconciler.js.map +1 -0
- package/dist/server/services/plan/types.d.ts +121 -0
- package/dist/server/services/plan/types.d.ts.map +1 -0
- package/dist/server/services/plan/types.js +4 -0
- package/dist/server/services/plan/types.js.map +1 -0
- package/dist/server/services/plan/watcher.d.ts +14 -0
- package/dist/server/services/plan/watcher.d.ts.map +1 -0
- package/dist/server/services/plan/watcher.js +69 -0
- package/dist/server/services/plan/watcher.js.map +1 -0
- package/dist/server/services/websocket/file-explorer-handlers.js +20 -0
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +21 -0
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/plan-handlers.d.ts +6 -0
- package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-handlers.js +494 -0
- package/dist/server/services/websocket/plan-handlers.js.map +1 -0
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +384 -12
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-persistence.d.ts +45 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-persistence.js +187 -0
- package/dist/server/services/websocket/quality-persistence.js.map +1 -0
- package/dist/server/services/websocket/quality-service.d.ts +12 -2
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-service.js +162 -18
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +2 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/server/cli/headless/claude-invoker.ts +25 -12
- package/server/cli/headless/headless-logger.ts +78 -0
- package/server/cli/headless/mcp-config.ts +6 -5
- package/server/cli/headless/runner.ts +4 -0
- package/server/cli/headless/stall-assessor.ts +131 -24
- package/server/cli/headless/tool-watchdog.ts +10 -9
- package/server/cli/headless/types.ts +10 -1
- package/server/cli/improvisation-session-manager.ts +118 -11
- package/server/mcp/bouncer-cli.ts +73 -0
- package/server/services/plan/composer.ts +199 -0
- package/server/services/plan/dependency-resolver.ts +182 -0
- package/server/services/plan/executor.ts +700 -0
- package/server/services/plan/parser.ts +491 -0
- package/server/services/plan/state-reconciler.ts +174 -0
- package/server/services/plan/types.ts +166 -0
- package/server/services/plan/watcher.ts +73 -0
- package/server/services/websocket/file-explorer-handlers.ts +20 -0
- package/server/services/websocket/handler.ts +21 -0
- package/server/services/websocket/plan-handlers.ts +592 -0
- package/server/services/websocket/quality-handlers.ts +450 -12
- package/server/services/websocket/quality-persistence.ts +250 -0
- package/server/services/websocket/quality-service.ts +183 -18
- package/server/services/websocket/types.ts +48 -2
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
/**
|
|
4
|
+
* Quality Persistence — Persists quality config, reports, and history
|
|
5
|
+
* to .mstro/quality/ in the working directory.
|
|
6
|
+
*
|
|
7
|
+
* Files:
|
|
8
|
+
* .mstro/quality/config.json — Directory list (paths + labels)
|
|
9
|
+
* .mstro/quality/reports/<slug>.json — Latest full report per directory
|
|
10
|
+
* .mstro/quality/history.json — Score history entries for trend tracking
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Helpers
|
|
16
|
+
// ============================================================================
|
|
17
|
+
const MAX_HISTORY_ENTRIES = 100;
|
|
18
|
+
function slugify(dirPath) {
|
|
19
|
+
if (dirPath === '.' || dirPath === './')
|
|
20
|
+
return '_root';
|
|
21
|
+
return dirPath.replace(/[/\\]/g, '_').replace(/^_+|_+$/g, '') || '_root';
|
|
22
|
+
}
|
|
23
|
+
function ensureDir(dir) {
|
|
24
|
+
if (!existsSync(dir)) {
|
|
25
|
+
mkdirSync(dir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function readJson(filePath, fallback) {
|
|
29
|
+
try {
|
|
30
|
+
if (existsSync(filePath)) {
|
|
31
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Corrupted or unreadable — return fallback
|
|
36
|
+
}
|
|
37
|
+
return fallback;
|
|
38
|
+
}
|
|
39
|
+
function writeJson(filePath, data) {
|
|
40
|
+
try {
|
|
41
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error('[QualityPersistence] Error writing:', filePath, error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// Quality Persistence
|
|
49
|
+
// ============================================================================
|
|
50
|
+
export class QualityPersistence {
|
|
51
|
+
qualityDir;
|
|
52
|
+
reportsDir;
|
|
53
|
+
configPath;
|
|
54
|
+
historyPath;
|
|
55
|
+
constructor(workingDir) {
|
|
56
|
+
this.qualityDir = join(workingDir, '.mstro', 'quality');
|
|
57
|
+
this.reportsDir = join(this.qualityDir, 'reports');
|
|
58
|
+
this.configPath = join(this.qualityDir, 'config.json');
|
|
59
|
+
this.historyPath = join(this.qualityDir, 'history.json');
|
|
60
|
+
ensureDir(this.reportsDir);
|
|
61
|
+
}
|
|
62
|
+
// ---- Config (directory list) ----
|
|
63
|
+
loadConfig() {
|
|
64
|
+
const config = readJson(this.configPath, { directories: [] });
|
|
65
|
+
return config.directories;
|
|
66
|
+
}
|
|
67
|
+
saveConfig(directories) {
|
|
68
|
+
writeJson(this.configPath, { directories });
|
|
69
|
+
}
|
|
70
|
+
addDirectory(path, label) {
|
|
71
|
+
const dirs = this.loadConfig();
|
|
72
|
+
if (!dirs.some((d) => d.path === path)) {
|
|
73
|
+
dirs.push({ path, label });
|
|
74
|
+
this.saveConfig(dirs);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
removeDirectory(path) {
|
|
78
|
+
const dirs = this.loadConfig().filter((d) => d.path !== path);
|
|
79
|
+
this.saveConfig(dirs);
|
|
80
|
+
}
|
|
81
|
+
// ---- Reports (latest per directory) ----
|
|
82
|
+
loadReport(dirPath) {
|
|
83
|
+
const slug = slugify(dirPath);
|
|
84
|
+
const reportPath = join(this.reportsDir, `${slug}.json`);
|
|
85
|
+
return readJson(reportPath, null);
|
|
86
|
+
}
|
|
87
|
+
saveReport(dirPath, results) {
|
|
88
|
+
const slug = slugify(dirPath);
|
|
89
|
+
const reportPath = join(this.reportsDir, `${slug}.json`);
|
|
90
|
+
writeJson(reportPath, results);
|
|
91
|
+
}
|
|
92
|
+
loadAllReports(directories) {
|
|
93
|
+
const reports = {};
|
|
94
|
+
for (const dir of directories) {
|
|
95
|
+
const report = this.loadReport(dir.path);
|
|
96
|
+
if (report) {
|
|
97
|
+
reports[dir.path] = report;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return reports;
|
|
101
|
+
}
|
|
102
|
+
// ---- History (trend tracking) ----
|
|
103
|
+
loadHistory() {
|
|
104
|
+
const history = readJson(this.historyPath, { entries: [] });
|
|
105
|
+
return history.entries;
|
|
106
|
+
}
|
|
107
|
+
appendHistory(results, dirPath) {
|
|
108
|
+
const history = this.loadHistory();
|
|
109
|
+
// Find or create entry for this timestamp batch
|
|
110
|
+
// If the last entry was within 60 seconds, merge into it (for multi-dir scans)
|
|
111
|
+
const now = new Date();
|
|
112
|
+
const lastEntry = history[history.length - 1];
|
|
113
|
+
const lastTime = lastEntry ? new Date(lastEntry.timestamp).getTime() : 0;
|
|
114
|
+
const mergeWindow = 60_000; // 60 seconds
|
|
115
|
+
const dirEntry = {
|
|
116
|
+
path: dirPath,
|
|
117
|
+
score: results.overall,
|
|
118
|
+
grade: results.grade,
|
|
119
|
+
};
|
|
120
|
+
if (lastEntry && now.getTime() - lastTime < mergeWindow) {
|
|
121
|
+
// Merge: update or add this directory in the last entry
|
|
122
|
+
const existing = lastEntry.directories.findIndex((d) => d.path === dirPath);
|
|
123
|
+
if (existing >= 0) {
|
|
124
|
+
lastEntry.directories[existing] = dirEntry;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
lastEntry.directories.push(dirEntry);
|
|
128
|
+
}
|
|
129
|
+
// Recompute overall as average of all directories in this entry
|
|
130
|
+
const totalScore = lastEntry.directories.reduce((sum, d) => sum + d.score, 0);
|
|
131
|
+
lastEntry.overall = Math.round(totalScore / lastEntry.directories.length);
|
|
132
|
+
lastEntry.grade = gradeFromScore(lastEntry.overall);
|
|
133
|
+
lastEntry.timestamp = now.toISOString();
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// New entry
|
|
137
|
+
history.push({
|
|
138
|
+
timestamp: now.toISOString(),
|
|
139
|
+
overall: results.overall,
|
|
140
|
+
grade: results.grade,
|
|
141
|
+
directories: [dirEntry],
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
// Trim to max entries
|
|
145
|
+
while (history.length > MAX_HISTORY_ENTRIES) {
|
|
146
|
+
history.shift();
|
|
147
|
+
}
|
|
148
|
+
writeJson(this.historyPath, { entries: history });
|
|
149
|
+
}
|
|
150
|
+
// ---- Code Review (persisted per directory) ----
|
|
151
|
+
loadCodeReview(dirPath) {
|
|
152
|
+
const slug = slugify(dirPath);
|
|
153
|
+
const reviewPath = join(this.reportsDir, `${slug}-review.json`);
|
|
154
|
+
return readJson(reviewPath, null);
|
|
155
|
+
}
|
|
156
|
+
saveCodeReview(dirPath, findings, summary) {
|
|
157
|
+
const slug = slugify(dirPath);
|
|
158
|
+
const reviewPath = join(this.reportsDir, `${slug}-review.json`);
|
|
159
|
+
writeJson(reviewPath, { findings, summary, timestamp: new Date().toISOString() });
|
|
160
|
+
}
|
|
161
|
+
// ---- Full state load ----
|
|
162
|
+
loadState() {
|
|
163
|
+
const directories = this.loadConfig();
|
|
164
|
+
const reports = this.loadAllReports(directories);
|
|
165
|
+
const history = this.loadHistory();
|
|
166
|
+
// Merge persisted code reviews into reports
|
|
167
|
+
for (const dir of directories) {
|
|
168
|
+
const review = this.loadCodeReview(dir.path);
|
|
169
|
+
if (review && reports[dir.path]) {
|
|
170
|
+
reports[dir.path] = { ...reports[dir.path], codeReview: review.findings };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { directories, reports, history };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function gradeFromScore(score) {
|
|
177
|
+
if (score >= 90)
|
|
178
|
+
return 'A';
|
|
179
|
+
if (score >= 80)
|
|
180
|
+
return 'B';
|
|
181
|
+
if (score >= 70)
|
|
182
|
+
return 'C';
|
|
183
|
+
if (score >= 60)
|
|
184
|
+
return 'D';
|
|
185
|
+
return 'F';
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=quality-persistence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quality-persistence.js","sourceRoot":"","sources":["../../../../server/services/websocket/quality-persistence.ts"],"names":[],"mappings":"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"}
|
|
@@ -38,7 +38,7 @@ export interface ScanProgress {
|
|
|
38
38
|
current: number;
|
|
39
39
|
total: number;
|
|
40
40
|
}
|
|
41
|
-
type Ecosystem = 'node' | 'python' | 'rust' | 'go' | 'unknown';
|
|
41
|
+
type Ecosystem = 'node' | 'python' | 'rust' | 'go' | 'swift' | 'kotlin' | 'unknown';
|
|
42
42
|
export declare function detectEcosystem(dirPath: string): Ecosystem[];
|
|
43
43
|
export declare function detectTools(dirPath: string): Promise<{
|
|
44
44
|
tools: QualityTool[];
|
|
@@ -48,7 +48,17 @@ export declare function installTools(dirPath: string, toolNames?: string[]): Pro
|
|
|
48
48
|
tools: QualityTool[];
|
|
49
49
|
ecosystem: string[];
|
|
50
50
|
}>;
|
|
51
|
+
export declare function computeAiReviewScore(findings: Array<{
|
|
52
|
+
severity: string;
|
|
53
|
+
}>, totalLines: number): number;
|
|
51
54
|
export type ProgressCallback = (progress: ScanProgress) => void;
|
|
52
|
-
export declare function runQualityScan(dirPath: string, onProgress?: ProgressCallback): Promise<QualityResults>;
|
|
55
|
+
export declare function runQualityScan(dirPath: string, onProgress?: ProgressCallback, installedToolNames?: string[]): Promise<QualityResults>;
|
|
56
|
+
/**
|
|
57
|
+
* Recompute the overall score after AI code review findings become available.
|
|
58
|
+
* Returns a new QualityResults with the AI Review category enabled and score updated.
|
|
59
|
+
*/
|
|
60
|
+
export declare function recomputeWithAiReview(results: QualityResults, aiFindings: Array<{
|
|
61
|
+
severity: string;
|
|
62
|
+
}>): QualityResults;
|
|
53
63
|
export {};
|
|
54
64
|
//# sourceMappingURL=quality-service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quality-service.d.ts","sourceRoot":"","sources":["../../../../server/services/websocket/quality-service.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,QAAQ,GAAG,WAAW,GAAG,YAAY,GAAG,SAAS,CAAC;CAC7D;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,KAAK,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"quality-service.d.ts","sourceRoot":"","sources":["../../../../server/services/websocket/quality-service.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,QAAQ,GAAG,WAAW,GAAG,YAAY,GAAG,SAAS,CAAC;CAC7D;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,KAAK,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;AAqFpF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAe5D;AAyCD,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAuBzG;AAMD,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC;IAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAiCxD;AAonBD,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,EACrC,UAAU,EAAE,MAAM,GACjB,MAAM,CAUR;AAqBD,MAAM,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;AAEhE,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,EACf,UAAU,CAAC,EAAE,gBAAgB,EAC7B,kBAAkB,CAAC,EAAE,MAAM,EAAE,GAC5B,OAAO,CAAC,cAAc,CAAC,CAiHzB;AAMD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,GACtC,cAAc,CA8BhB"}
|
|
@@ -14,9 +14,9 @@ const ECOSYSTEM_TOOLS = {
|
|
|
14
14
|
{ name: 'typescript', check: ['npx', 'tsc', '--version'], category: 'general', installCmd: 'npm install -D typescript' },
|
|
15
15
|
],
|
|
16
16
|
python: [
|
|
17
|
-
{ name: 'ruff', check: ['ruff', '--version'], category: 'linter', installCmd: 'pip install ruff' },
|
|
18
|
-
{ name: 'black', check: ['black', '--version'], category: 'formatter', installCmd: 'pip install black' },
|
|
19
|
-
{ name: 'radon', check: ['radon', '--version'], category: 'complexity', installCmd: 'pip install radon' },
|
|
17
|
+
{ name: 'ruff', check: ['ruff', '--version'], category: 'linter', installCmd: 'uv tool install ruff || pip install ruff' },
|
|
18
|
+
{ name: 'black', check: ['black', '--version'], category: 'formatter', installCmd: 'uv tool install black || pip install black' },
|
|
19
|
+
{ name: 'radon', check: ['radon', '--version'], category: 'complexity', installCmd: 'uv tool install radon || pip install radon' },
|
|
20
20
|
],
|
|
21
21
|
rust: [
|
|
22
22
|
{ name: 'clippy', check: ['cargo', 'clippy', '--version'], category: 'linter', installCmd: 'rustup component add clippy' },
|
|
@@ -26,6 +26,14 @@ const ECOSYSTEM_TOOLS = {
|
|
|
26
26
|
{ name: 'golangci-lint', check: ['golangci-lint', '--version'], category: 'linter', installCmd: 'go install github.com/golangci-lint/golangci-lint/cmd/golangci-lint@latest' },
|
|
27
27
|
{ name: 'gofmt', check: ['gofmt', '-h'], category: 'formatter', installCmd: '(built-in with Go)' },
|
|
28
28
|
],
|
|
29
|
+
swift: [
|
|
30
|
+
{ name: 'swiftlint', check: ['swiftlint', '--version'], category: 'linter', installCmd: 'brew install swiftlint' },
|
|
31
|
+
{ name: 'swiftformat', check: ['swiftformat', '--version'], category: 'formatter', installCmd: 'brew install swiftformat' },
|
|
32
|
+
],
|
|
33
|
+
kotlin: [
|
|
34
|
+
{ name: 'ktlint', check: ['ktlint', '--version'], category: 'linter', installCmd: 'brew install ktlint' },
|
|
35
|
+
{ name: 'ktfmt', check: ['ktfmt', '--version'], category: 'formatter', installCmd: 'brew install ktfmt' },
|
|
36
|
+
],
|
|
29
37
|
unknown: [],
|
|
30
38
|
};
|
|
31
39
|
const SOURCE_EXTENSIONS = new Set([
|
|
@@ -48,6 +56,16 @@ const IGNORE_DIRS = new Set([
|
|
|
48
56
|
const FILE_LENGTH_THRESHOLD = 300;
|
|
49
57
|
const FUNCTION_LENGTH_THRESHOLD = 50;
|
|
50
58
|
const TOTAL_STEPS = 7;
|
|
59
|
+
function hasInstalledToolInCategory(installedSet, ecosystems, category) {
|
|
60
|
+
for (const eco of ecosystems) {
|
|
61
|
+
const specs = ECOSYSTEM_TOOLS[eco] || [];
|
|
62
|
+
for (const spec of specs) {
|
|
63
|
+
if (spec.category === category && installedSet.has(spec.name))
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
51
69
|
// ============================================================================
|
|
52
70
|
// Ecosystem Detection
|
|
53
71
|
// ============================================================================
|
|
@@ -63,6 +81,10 @@ export function detectEcosystem(dirPath) {
|
|
|
63
81
|
ecosystems.push('rust');
|
|
64
82
|
if (files.includes('go.mod'))
|
|
65
83
|
ecosystems.push('go');
|
|
84
|
+
if (files.includes('Package.swift') || files.some(f => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace')))
|
|
85
|
+
ecosystems.push('swift');
|
|
86
|
+
if (files.includes('build.gradle') || files.includes('build.gradle.kts'))
|
|
87
|
+
ecosystems.push('kotlin');
|
|
66
88
|
}
|
|
67
89
|
catch {
|
|
68
90
|
// Directory not readable
|
|
@@ -71,6 +93,31 @@ export function detectEcosystem(dirPath) {
|
|
|
71
93
|
ecosystems.push('unknown');
|
|
72
94
|
return ecosystems;
|
|
73
95
|
}
|
|
96
|
+
/** Detect the Node.js package manager from lockfiles */
|
|
97
|
+
function detectNodePackageManager(dirPath) {
|
|
98
|
+
try {
|
|
99
|
+
const files = readdirSync(dirPath);
|
|
100
|
+
if (files.includes('bun.lockb') || files.includes('bun.lock'))
|
|
101
|
+
return 'bun';
|
|
102
|
+
if (files.includes('pnpm-lock.yaml'))
|
|
103
|
+
return 'pnpm';
|
|
104
|
+
if (files.includes('yarn.lock'))
|
|
105
|
+
return 'yarn';
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Directory not readable
|
|
109
|
+
}
|
|
110
|
+
return 'npm';
|
|
111
|
+
}
|
|
112
|
+
/** Build the install command for a Node.js dev dependency */
|
|
113
|
+
function nodeInstallCmd(pm, pkg) {
|
|
114
|
+
switch (pm) {
|
|
115
|
+
case 'yarn': return `yarn add -D ${pkg}`;
|
|
116
|
+
case 'pnpm': return `pnpm add -D ${pkg}`;
|
|
117
|
+
case 'bun': return `bun add -d ${pkg}`;
|
|
118
|
+
default: return `npm install -D ${pkg}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
74
121
|
// ============================================================================
|
|
75
122
|
// Tool Detection
|
|
76
123
|
// ============================================================================
|
|
@@ -88,14 +135,19 @@ async function checkToolInstalled(check, cwd) {
|
|
|
88
135
|
export async function detectTools(dirPath) {
|
|
89
136
|
const ecosystems = detectEcosystem(dirPath);
|
|
90
137
|
const tools = [];
|
|
138
|
+
const nodePm = ecosystems.includes('node') ? detectNodePackageManager(dirPath) : 'npm';
|
|
91
139
|
for (const eco of ecosystems) {
|
|
92
140
|
const specs = ECOSYSTEM_TOOLS[eco] || [];
|
|
93
141
|
for (const spec of specs) {
|
|
94
142
|
const installed = await checkToolInstalled(spec.check, dirPath);
|
|
143
|
+
// For node tools, resolve install command using the project's package manager
|
|
144
|
+
const installCommand = eco === 'node'
|
|
145
|
+
? nodeInstallCmd(nodePm, spec.installCmd.replace(/^npm install -D /, ''))
|
|
146
|
+
: spec.installCmd;
|
|
95
147
|
tools.push({
|
|
96
148
|
name: spec.name,
|
|
97
149
|
installed,
|
|
98
|
-
installCommand
|
|
150
|
+
installCommand,
|
|
99
151
|
category: spec.category,
|
|
100
152
|
});
|
|
101
153
|
}
|
|
@@ -108,14 +160,35 @@ export async function detectTools(dirPath) {
|
|
|
108
160
|
export async function installTools(dirPath, toolNames) {
|
|
109
161
|
const { tools } = await detectTools(dirPath);
|
|
110
162
|
const toInstall = tools.filter((t) => !t.installed && (!toolNames || toolNames.includes(t.name)));
|
|
163
|
+
const failures = [];
|
|
111
164
|
for (const tool of toInstall) {
|
|
112
165
|
if (tool.installCommand.startsWith('('))
|
|
113
166
|
continue; // built-in, skip
|
|
114
|
-
|
|
115
|
-
|
|
167
|
+
// Support chained commands with || (try first, fallback to second)
|
|
168
|
+
const commands = tool.installCommand.split(' || ');
|
|
169
|
+
let installed = false;
|
|
170
|
+
for (const cmd of commands) {
|
|
171
|
+
const parts = cmd.trim().split(' ');
|
|
172
|
+
const result = await runCommand(parts[0], parts.slice(1), dirPath);
|
|
173
|
+
if (result.exitCode === 0) {
|
|
174
|
+
installed = true;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!installed) {
|
|
179
|
+
failures.push(`${tool.name}: all install methods failed`);
|
|
180
|
+
}
|
|
116
181
|
}
|
|
117
182
|
// Re-detect after install
|
|
118
|
-
|
|
183
|
+
const detected = await detectTools(dirPath);
|
|
184
|
+
// Check if any requested tools are still missing after install
|
|
185
|
+
const requestedNames = new Set(toolNames ?? toInstall.map((t) => t.name));
|
|
186
|
+
const stillMissing = detected.tools.filter((t) => !t.installed && requestedNames.has(t.name)).map((t) => t.name);
|
|
187
|
+
if (stillMissing.length > 0) {
|
|
188
|
+
const detail = failures.length > 0 ? ` ${failures.join('; ')}` : '';
|
|
189
|
+
throw new Error(`Failed to install: ${stillMissing.join(', ')}.${detail}`);
|
|
190
|
+
}
|
|
191
|
+
return detected;
|
|
119
192
|
}
|
|
120
193
|
function tryStatSync(path) {
|
|
121
194
|
try {
|
|
@@ -291,7 +364,7 @@ async function lintNode(dirPath, acc) {
|
|
|
291
364
|
}
|
|
292
365
|
async function lintPython(dirPath, acc) {
|
|
293
366
|
const result = await runCommand('ruff', ['check', '--output-format=json', '.'], dirPath);
|
|
294
|
-
if (result.exitCode
|
|
367
|
+
if (result.exitCode !== 0 && !result.stdout.trim().startsWith('['))
|
|
295
368
|
return;
|
|
296
369
|
acc.ran = true;
|
|
297
370
|
try {
|
|
@@ -658,12 +731,32 @@ function computeGrade(score) {
|
|
|
658
731
|
return 'F';
|
|
659
732
|
}
|
|
660
733
|
const DEFAULT_WEIGHTS = {
|
|
661
|
-
linting: 0.
|
|
662
|
-
formatting: 0.
|
|
663
|
-
complexity: 0.
|
|
664
|
-
fileLength: 0.
|
|
665
|
-
functionLength: 0.
|
|
734
|
+
linting: 0.25,
|
|
735
|
+
formatting: 0.10,
|
|
736
|
+
complexity: 0.20,
|
|
737
|
+
fileLength: 0.12,
|
|
738
|
+
functionLength: 0.13,
|
|
739
|
+
aiReview: 0.20,
|
|
666
740
|
};
|
|
741
|
+
// ============================================================================
|
|
742
|
+
// AI Code Review Score
|
|
743
|
+
// ============================================================================
|
|
744
|
+
const SEVERITY_PENALTY = {
|
|
745
|
+
critical: 10.0,
|
|
746
|
+
high: 5.0,
|
|
747
|
+
medium: 2.0,
|
|
748
|
+
low: 0.5,
|
|
749
|
+
};
|
|
750
|
+
/** Exponential decay constant — higher = harsher scoring */
|
|
751
|
+
const AI_REVIEW_DECAY = 0.10;
|
|
752
|
+
export function computeAiReviewScore(findings, totalLines) {
|
|
753
|
+
if (findings.length === 0)
|
|
754
|
+
return 100;
|
|
755
|
+
const effectiveKloc = Math.max(totalLines / 1000, 1.0);
|
|
756
|
+
const totalPenalty = findings.reduce((sum, f) => sum + (SEVERITY_PENALTY[f.severity] ?? 2.0), 0);
|
|
757
|
+
const penaltyDensity = totalPenalty / effectiveKloc;
|
|
758
|
+
return Math.round(100 * Math.exp(-AI_REVIEW_DECAY * penaltyDensity));
|
|
759
|
+
}
|
|
667
760
|
function computeOverallScore(categories) {
|
|
668
761
|
const available = categories.filter((c) => c.available);
|
|
669
762
|
if (available.length === 0)
|
|
@@ -677,20 +770,28 @@ function computeOverallScore(categories) {
|
|
|
677
770
|
}
|
|
678
771
|
return Math.round(Math.max(0, Math.min(100, weighted)));
|
|
679
772
|
}
|
|
680
|
-
export async function runQualityScan(dirPath, onProgress) {
|
|
773
|
+
export async function runQualityScan(dirPath, onProgress, installedToolNames) {
|
|
681
774
|
const ecosystems = detectEcosystem(dirPath);
|
|
775
|
+
// Build set of installed tools for gating analyses
|
|
776
|
+
const installedSet = installedToolNames ? new Set(installedToolNames) : null;
|
|
682
777
|
const progress = (step, current) => {
|
|
683
778
|
onProgress?.({ step, current, total: TOTAL_STEPS });
|
|
684
779
|
};
|
|
685
780
|
// Step 1: Collect source files
|
|
686
781
|
progress('Collecting source files', 1);
|
|
687
782
|
const files = collectSourceFiles(dirPath, dirPath);
|
|
688
|
-
// Step 2: Run linting
|
|
783
|
+
// Step 2: Run linting (only if a linter is installed)
|
|
689
784
|
progress('Running linters', 2);
|
|
690
|
-
const
|
|
691
|
-
|
|
785
|
+
const hasLinter = !installedSet || hasInstalledToolInCategory(installedSet, ecosystems, 'linter');
|
|
786
|
+
const lintResult = hasLinter
|
|
787
|
+
? await analyzeLinting(dirPath, ecosystems, files)
|
|
788
|
+
: { score: 0, findings: [], available: false, issueCount: 0 };
|
|
789
|
+
// Step 3: Check formatting (only if a formatter is installed)
|
|
692
790
|
progress('Checking formatting', 3);
|
|
693
|
-
const
|
|
791
|
+
const hasFormatter = !installedSet || hasInstalledToolInCategory(installedSet, ecosystems, 'formatter');
|
|
792
|
+
const fmtResult = hasFormatter
|
|
793
|
+
? await analyzeFormatting(dirPath, ecosystems, files)
|
|
794
|
+
: { score: 0, available: false, issueCount: 0 };
|
|
694
795
|
// Step 4: Analyze complexity
|
|
695
796
|
progress('Analyzing complexity', 4);
|
|
696
797
|
const complexityResult = analyzeComplexity(files);
|
|
@@ -743,6 +844,14 @@ export async function runQualityScan(dirPath, onProgress) {
|
|
|
743
844
|
available: true,
|
|
744
845
|
issueCount: funcLengthResult.issueCount,
|
|
745
846
|
},
|
|
847
|
+
{
|
|
848
|
+
name: 'AI Review',
|
|
849
|
+
score: 0,
|
|
850
|
+
weight: DEFAULT_WEIGHTS.aiReview,
|
|
851
|
+
effectiveWeight: DEFAULT_WEIGHTS.aiReview,
|
|
852
|
+
available: false,
|
|
853
|
+
issueCount: 0,
|
|
854
|
+
},
|
|
746
855
|
];
|
|
747
856
|
const overall = computeOverallScore(categories);
|
|
748
857
|
const allFindings = [
|
|
@@ -763,4 +872,39 @@ export async function runQualityScan(dirPath, onProgress) {
|
|
|
763
872
|
ecosystem: ecosystems,
|
|
764
873
|
};
|
|
765
874
|
}
|
|
875
|
+
// ============================================================================
|
|
876
|
+
// Recompute with AI Review
|
|
877
|
+
// ============================================================================
|
|
878
|
+
/**
|
|
879
|
+
* Recompute the overall score after AI code review findings become available.
|
|
880
|
+
* Returns a new QualityResults with the AI Review category enabled and score updated.
|
|
881
|
+
*/
|
|
882
|
+
export function recomputeWithAiReview(results, aiFindings) {
|
|
883
|
+
const aiScore = computeAiReviewScore(aiFindings, results.totalLines);
|
|
884
|
+
// Update or add the AI Review category
|
|
885
|
+
const categories = results.categories.map((cat) => ({ ...cat }));
|
|
886
|
+
const aiCatIndex = categories.findIndex((c) => c.name === 'AI Review');
|
|
887
|
+
const aiCategory = {
|
|
888
|
+
name: 'AI Review',
|
|
889
|
+
score: aiScore,
|
|
890
|
+
weight: DEFAULT_WEIGHTS.aiReview,
|
|
891
|
+
effectiveWeight: DEFAULT_WEIGHTS.aiReview,
|
|
892
|
+
available: true,
|
|
893
|
+
issueCount: aiFindings.length,
|
|
894
|
+
};
|
|
895
|
+
if (aiCatIndex >= 0) {
|
|
896
|
+
categories[aiCatIndex] = aiCategory;
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
categories.push(aiCategory);
|
|
900
|
+
}
|
|
901
|
+
const overall = computeOverallScore(categories);
|
|
902
|
+
return {
|
|
903
|
+
...results,
|
|
904
|
+
overall,
|
|
905
|
+
grade: computeGrade(overall),
|
|
906
|
+
categories,
|
|
907
|
+
codeReview: results.codeReview,
|
|
908
|
+
};
|
|
909
|
+
}
|
|
766
910
|
//# sourceMappingURL=quality-service.js.map
|