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.
Files changed (109) hide show
  1. package/LICENSE +191 -21
  2. package/PRIVACY.md +286 -62
  3. package/README.md +81 -58
  4. package/bin/commands/status.js +1 -1
  5. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  6. package/dist/server/cli/headless/claude-invoker.js +22 -12
  7. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  8. package/dist/server/cli/headless/headless-logger.d.ts +10 -0
  9. package/dist/server/cli/headless/headless-logger.d.ts.map +1 -0
  10. package/dist/server/cli/headless/headless-logger.js +66 -0
  11. package/dist/server/cli/headless/headless-logger.js.map +1 -0
  12. package/dist/server/cli/headless/mcp-config.d.ts.map +1 -1
  13. package/dist/server/cli/headless/mcp-config.js +6 -5
  14. package/dist/server/cli/headless/mcp-config.js.map +1 -1
  15. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  16. package/dist/server/cli/headless/runner.js +4 -0
  17. package/dist/server/cli/headless/runner.js.map +1 -1
  18. package/dist/server/cli/headless/stall-assessor.d.ts +21 -0
  19. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  20. package/dist/server/cli/headless/stall-assessor.js +100 -24
  21. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  22. package/dist/server/cli/headless/tool-watchdog.d.ts +0 -12
  23. package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
  24. package/dist/server/cli/headless/tool-watchdog.js +22 -9
  25. package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
  26. package/dist/server/cli/headless/types.d.ts +8 -1
  27. package/dist/server/cli/headless/types.d.ts.map +1 -1
  28. package/dist/server/cli/improvisation-session-manager.d.ts +16 -0
  29. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  30. package/dist/server/cli/improvisation-session-manager.js +94 -11
  31. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  32. package/dist/server/mcp/bouncer-cli.d.ts +3 -0
  33. package/dist/server/mcp/bouncer-cli.d.ts.map +1 -0
  34. package/dist/server/mcp/bouncer-cli.js +54 -0
  35. package/dist/server/mcp/bouncer-cli.js.map +1 -0
  36. package/dist/server/services/plan/composer.d.ts +4 -0
  37. package/dist/server/services/plan/composer.d.ts.map +1 -0
  38. package/dist/server/services/plan/composer.js +181 -0
  39. package/dist/server/services/plan/composer.js.map +1 -0
  40. package/dist/server/services/plan/dependency-resolver.d.ts +28 -0
  41. package/dist/server/services/plan/dependency-resolver.d.ts.map +1 -0
  42. package/dist/server/services/plan/dependency-resolver.js +154 -0
  43. package/dist/server/services/plan/dependency-resolver.js.map +1 -0
  44. package/dist/server/services/plan/executor.d.ts +110 -0
  45. package/dist/server/services/plan/executor.d.ts.map +1 -0
  46. package/dist/server/services/plan/executor.js +641 -0
  47. package/dist/server/services/plan/executor.js.map +1 -0
  48. package/dist/server/services/plan/parser.d.ts +11 -0
  49. package/dist/server/services/plan/parser.d.ts.map +1 -0
  50. package/dist/server/services/plan/parser.js +445 -0
  51. package/dist/server/services/plan/parser.js.map +1 -0
  52. package/dist/server/services/plan/state-reconciler.d.ts +2 -0
  53. package/dist/server/services/plan/state-reconciler.d.ts.map +1 -0
  54. package/dist/server/services/plan/state-reconciler.js +145 -0
  55. package/dist/server/services/plan/state-reconciler.js.map +1 -0
  56. package/dist/server/services/plan/types.d.ts +121 -0
  57. package/dist/server/services/plan/types.d.ts.map +1 -0
  58. package/dist/server/services/plan/types.js +4 -0
  59. package/dist/server/services/plan/types.js.map +1 -0
  60. package/dist/server/services/plan/watcher.d.ts +14 -0
  61. package/dist/server/services/plan/watcher.d.ts.map +1 -0
  62. package/dist/server/services/plan/watcher.js +69 -0
  63. package/dist/server/services/plan/watcher.js.map +1 -0
  64. package/dist/server/services/websocket/file-explorer-handlers.js +20 -0
  65. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  66. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  67. package/dist/server/services/websocket/handler.js +21 -0
  68. package/dist/server/services/websocket/handler.js.map +1 -1
  69. package/dist/server/services/websocket/plan-handlers.d.ts +6 -0
  70. package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -0
  71. package/dist/server/services/websocket/plan-handlers.js +494 -0
  72. package/dist/server/services/websocket/plan-handlers.js.map +1 -0
  73. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
  74. package/dist/server/services/websocket/quality-handlers.js +384 -12
  75. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  76. package/dist/server/services/websocket/quality-persistence.d.ts +45 -0
  77. package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -0
  78. package/dist/server/services/websocket/quality-persistence.js +187 -0
  79. package/dist/server/services/websocket/quality-persistence.js.map +1 -0
  80. package/dist/server/services/websocket/quality-service.d.ts +12 -2
  81. package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
  82. package/dist/server/services/websocket/quality-service.js +162 -18
  83. package/dist/server/services/websocket/quality-service.js.map +1 -1
  84. package/dist/server/services/websocket/types.d.ts +2 -2
  85. package/dist/server/services/websocket/types.d.ts.map +1 -1
  86. package/package.json +3 -3
  87. package/server/cli/headless/claude-invoker.ts +25 -12
  88. package/server/cli/headless/headless-logger.ts +78 -0
  89. package/server/cli/headless/mcp-config.ts +6 -5
  90. package/server/cli/headless/runner.ts +4 -0
  91. package/server/cli/headless/stall-assessor.ts +131 -24
  92. package/server/cli/headless/tool-watchdog.ts +10 -9
  93. package/server/cli/headless/types.ts +10 -1
  94. package/server/cli/improvisation-session-manager.ts +118 -11
  95. package/server/mcp/bouncer-cli.ts +73 -0
  96. package/server/services/plan/composer.ts +199 -0
  97. package/server/services/plan/dependency-resolver.ts +182 -0
  98. package/server/services/plan/executor.ts +700 -0
  99. package/server/services/plan/parser.ts +491 -0
  100. package/server/services/plan/state-reconciler.ts +174 -0
  101. package/server/services/plan/types.ts +166 -0
  102. package/server/services/plan/watcher.ts +73 -0
  103. package/server/services/websocket/file-explorer-handlers.ts +20 -0
  104. package/server/services/websocket/handler.ts +21 -0
  105. package/server/services/websocket/plan-handlers.ts +592 -0
  106. package/server/services/websocket/quality-handlers.ts +450 -12
  107. package/server/services/websocket/quality-persistence.ts +250 -0
  108. package/server/services/websocket/quality-service.ts +183 -18
  109. 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;AA+D/D,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAa5D;AAkBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAkBzG;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,CAYxD;AAunBD,MAAM,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;AAEhE,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,EACf,UAAU,CAAC,EAAE,gBAAgB,GAC5B,OAAO,CAAC,cAAc,CAAC,CAgGzB"}
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: spec.installCmd,
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
- const parts = tool.installCommand.split(' ');
115
- await runCommand(parts[0], parts.slice(1), dirPath);
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
- return detectTools(dirPath);
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 > 1)
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.30,
662
- formatting: 0.15,
663
- complexity: 0.25,
664
- fileLength: 0.15,
665
- functionLength: 0.15,
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 lintResult = await analyzeLinting(dirPath, ecosystems, files);
691
- // Step 3: Check formatting
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 fmtResult = await analyzeFormatting(dirPath, ecosystems, files);
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