mason-context 0.3.7 → 0.7.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/README.md +263 -61
- package/dist/mason-audit.js +1366 -0
- package/dist/mason-audit.js.map +1 -0
- package/dist/mason-drift.js +541 -0
- package/dist/mason-drift.js.map +1 -0
- package/dist/mason-mcp.js +4283 -0
- package/dist/mason-mcp.js.map +1 -0
- package/dist/mason.js +19 -0
- package/dist/mason.js.map +1 -0
- package/package.json +12 -11
- package/dist/bin/mason-mcp.js +0 -1412
- package/dist/bin/mason-mcp.js.map +0 -1
- package/dist/bin/mason.js +0 -2338
- package/dist/bin/mason.js.map +0 -1
- package/dist/src/cli.js +0 -2337
- package/dist/src/cli.js.map +0 -1
package/dist/bin/mason-mcp.js
DELETED
|
@@ -1,1412 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
-
var __esm = (fn, res) => function __init() {
|
|
5
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
-
};
|
|
7
|
-
var __export = (target, all) => {
|
|
8
|
-
for (var name in all)
|
|
9
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
// src/test-map.ts
|
|
13
|
-
var test_map_exports = {};
|
|
14
|
-
__export(test_map_exports, {
|
|
15
|
-
buildTestMap: () => buildTestMap
|
|
16
|
-
});
|
|
17
|
-
import path2 from "path";
|
|
18
|
-
import fg3 from "fast-glob";
|
|
19
|
-
async function buildTestMap(dir) {
|
|
20
|
-
const rootDir = path2.resolve(dir);
|
|
21
|
-
const testPatterns = [
|
|
22
|
-
"**/*.test.*",
|
|
23
|
-
"**/*.spec.*",
|
|
24
|
-
"**/*Test.kt",
|
|
25
|
-
"**/*Test.java",
|
|
26
|
-
"**/*Tests.kt",
|
|
27
|
-
"**/*Tests.java",
|
|
28
|
-
"**/test_*.py",
|
|
29
|
-
"**/*_test.py",
|
|
30
|
-
"**/*_test.go",
|
|
31
|
-
"**/*Tests.swift",
|
|
32
|
-
"**/*Test.swift",
|
|
33
|
-
"**/*_test.rs"
|
|
34
|
-
];
|
|
35
|
-
const testFiles = await fg3(testPatterns, { cwd: rootDir, ignore: IGNORE });
|
|
36
|
-
const sourceFiles = await fg3(
|
|
37
|
-
"**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,dart}",
|
|
38
|
-
{ cwd: rootDir, ignore: IGNORE }
|
|
39
|
-
);
|
|
40
|
-
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
41
|
-
for (const file of sourceFiles) {
|
|
42
|
-
if (testFiles.includes(file)) continue;
|
|
43
|
-
const baseName = path2.basename(file).replace(/\.[^.]+$/, "");
|
|
44
|
-
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
45
|
-
existing.push(file);
|
|
46
|
-
sourceByBaseName.set(baseName, existing);
|
|
47
|
-
}
|
|
48
|
-
const paired = [];
|
|
49
|
-
const unmatched = [];
|
|
50
|
-
for (const testFile of testFiles) {
|
|
51
|
-
const testBaseName = path2.basename(testFile).replace(/\.[^.]+$/, "");
|
|
52
|
-
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
53
|
-
if (!sourceName) {
|
|
54
|
-
unmatched.push(testFile);
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
const candidates = sourceByBaseName.get(sourceName);
|
|
58
|
-
if (candidates && candidates.length > 0) {
|
|
59
|
-
const testDir = path2.dirname(testFile);
|
|
60
|
-
const bestMatch = candidates.reduce((best, candidate) => {
|
|
61
|
-
const candidateDir = path2.dirname(candidate);
|
|
62
|
-
const bestDir = path2.dirname(best);
|
|
63
|
-
const candidateOverlap = commonSegments(testDir, candidateDir);
|
|
64
|
-
const bestOverlap = commonSegments(testDir, bestDir);
|
|
65
|
-
return candidateOverlap > bestOverlap ? candidate : best;
|
|
66
|
-
});
|
|
67
|
-
paired.push({
|
|
68
|
-
test: testFile,
|
|
69
|
-
source: bestMatch,
|
|
70
|
-
confidence: candidates.length === 1 ? "exact" : "best-guess"
|
|
71
|
-
});
|
|
72
|
-
} else {
|
|
73
|
-
unmatched.push(testFile);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return { totalTestFiles: testFiles.length, paired, unmatched };
|
|
77
|
-
}
|
|
78
|
-
function commonSegments(pathA, pathB) {
|
|
79
|
-
const segsA = pathA.split("/");
|
|
80
|
-
const segsB = pathB.split("/");
|
|
81
|
-
let count = 0;
|
|
82
|
-
for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
|
|
83
|
-
if (segsA[i] === segsB[i]) count++;
|
|
84
|
-
else break;
|
|
85
|
-
}
|
|
86
|
-
return count;
|
|
87
|
-
}
|
|
88
|
-
var IGNORE;
|
|
89
|
-
var init_test_map = __esm({
|
|
90
|
-
"src/test-map.ts"() {
|
|
91
|
-
"use strict";
|
|
92
|
-
IGNORE = [
|
|
93
|
-
"**/node_modules/**",
|
|
94
|
-
"**/dist/**",
|
|
95
|
-
"**/build/**",
|
|
96
|
-
"**/.gradle/**",
|
|
97
|
-
"**/target/**",
|
|
98
|
-
"**/.git/**",
|
|
99
|
-
"**/vendor/**",
|
|
100
|
-
"**/__pycache__/**",
|
|
101
|
-
"**/venv/**",
|
|
102
|
-
"**/.venv/**",
|
|
103
|
-
"**/*.min.*",
|
|
104
|
-
"**/*.map"
|
|
105
|
-
];
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// src/impact/impact.ts
|
|
110
|
-
var impact_exports = {};
|
|
111
|
-
__export(impact_exports, {
|
|
112
|
-
analyzeImpact: () => analyzeImpact
|
|
113
|
-
});
|
|
114
|
-
import fs5 from "fs/promises";
|
|
115
|
-
import path5 from "path";
|
|
116
|
-
import { execFile as execFile7 } from "child_process";
|
|
117
|
-
import { promisify as promisify7 } from "util";
|
|
118
|
-
import fg5 from "fast-glob";
|
|
119
|
-
async function analyzeImpact(rootDir, targetFiles) {
|
|
120
|
-
const resolvedRoot = path5.resolve(rootDir);
|
|
121
|
-
const resolvedTargets = await resolveTargetFiles(resolvedRoot, targetFiles);
|
|
122
|
-
const [cochange, references, tests] = await Promise.all([
|
|
123
|
-
getCochangeFiles(resolvedRoot, resolvedTargets),
|
|
124
|
-
getReferences(resolvedRoot, resolvedTargets),
|
|
125
|
-
getRelatedTests(resolvedRoot, resolvedTargets)
|
|
126
|
-
]);
|
|
127
|
-
return {
|
|
128
|
-
targetFiles: resolvedTargets,
|
|
129
|
-
cochange,
|
|
130
|
-
references,
|
|
131
|
-
tests
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
async function resolveTargetFiles(rootDir, targets) {
|
|
135
|
-
const resolved = [];
|
|
136
|
-
for (const target of targets) {
|
|
137
|
-
if (target.includes("/")) {
|
|
138
|
-
resolved.push(target);
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
const matches = await fg5(`**/${target}`, {
|
|
142
|
-
cwd: rootDir,
|
|
143
|
-
ignore: IGNORE2
|
|
144
|
-
});
|
|
145
|
-
if (matches.length > 0) {
|
|
146
|
-
resolved.push(matches[0]);
|
|
147
|
-
} else {
|
|
148
|
-
const noExt = target.replace(/\.[^.]+$/, "");
|
|
149
|
-
const extMatches = await fg5(`**/${noExt}.*`, {
|
|
150
|
-
cwd: rootDir,
|
|
151
|
-
ignore: IGNORE2
|
|
152
|
-
});
|
|
153
|
-
if (extMatches.length > 0) {
|
|
154
|
-
resolved.push(extMatches[0]);
|
|
155
|
-
} else {
|
|
156
|
-
resolved.push(target);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return resolved;
|
|
161
|
-
}
|
|
162
|
-
async function getCochangeFiles(rootDir, targetFiles) {
|
|
163
|
-
const cochangeCounts = /* @__PURE__ */ new Map();
|
|
164
|
-
let totalTargetCommits = 0;
|
|
165
|
-
for (const targetFile of targetFiles) {
|
|
166
|
-
try {
|
|
167
|
-
const { stdout: commitLog } = await exec7(
|
|
168
|
-
"git",
|
|
169
|
-
["log", "--format=%H", "-n", "500", "--", targetFile],
|
|
170
|
-
{ cwd: rootDir, maxBuffer: 5e6 }
|
|
171
|
-
);
|
|
172
|
-
const commits = commitLog.trim().split("\n").filter(Boolean);
|
|
173
|
-
totalTargetCommits += commits.length;
|
|
174
|
-
if (commits.length === 0) continue;
|
|
175
|
-
for (const commit of commits) {
|
|
176
|
-
try {
|
|
177
|
-
const { stdout: filesInCommit } = await exec7(
|
|
178
|
-
"git",
|
|
179
|
-
["diff-tree", "--no-commit-id", "--name-only", "-r", commit],
|
|
180
|
-
{ cwd: rootDir }
|
|
181
|
-
);
|
|
182
|
-
const files = filesInCommit.trim().split("\n").filter(Boolean);
|
|
183
|
-
for (const file of files) {
|
|
184
|
-
if (targetFiles.includes(file)) continue;
|
|
185
|
-
cochangeCounts.set(file, (cochangeCounts.get(file) ?? 0) + 1);
|
|
186
|
-
}
|
|
187
|
-
} catch {
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
} catch {
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
if (totalTargetCommits === 0) return [];
|
|
194
|
-
return [...cochangeCounts.entries()].map(([file, count]) => ({
|
|
195
|
-
file,
|
|
196
|
-
cochangeRate: Math.round(count / totalTargetCommits * 100) / 100,
|
|
197
|
-
sharedCommits: count
|
|
198
|
-
})).filter((e) => e.cochangeRate >= 0.3 || e.sharedCommits >= 3).sort((a, b) => b.cochangeRate - a.cochangeRate).slice(0, 20);
|
|
199
|
-
}
|
|
200
|
-
async function getReferences(rootDir, targetFiles) {
|
|
201
|
-
const searchNames = /* @__PURE__ */ new Set();
|
|
202
|
-
for (const target of targetFiles) {
|
|
203
|
-
const basename = path5.basename(target).replace(/\.[^.]+$/, "");
|
|
204
|
-
searchNames.add(basename);
|
|
205
|
-
}
|
|
206
|
-
const allSourceFiles = await fg5(`**/${SOURCE_EXTENSIONS2}`, {
|
|
207
|
-
cwd: rootDir,
|
|
208
|
-
ignore: IGNORE2
|
|
209
|
-
});
|
|
210
|
-
const targetSet = new Set(targetFiles);
|
|
211
|
-
const filesToSearch = allSourceFiles.filter((f) => !targetSet.has(f));
|
|
212
|
-
const results = /* @__PURE__ */ new Map();
|
|
213
|
-
const batchSize = 50;
|
|
214
|
-
for (let i = 0; i < filesToSearch.length; i += batchSize) {
|
|
215
|
-
const batch = filesToSearch.slice(i, i + batchSize);
|
|
216
|
-
await Promise.all(
|
|
217
|
-
batch.map(async (file) => {
|
|
218
|
-
try {
|
|
219
|
-
const content = await fs5.readFile(
|
|
220
|
-
path5.join(rootDir, file),
|
|
221
|
-
"utf-8"
|
|
222
|
-
);
|
|
223
|
-
for (const name of searchNames) {
|
|
224
|
-
const regex = new RegExp(`\\b${escapeRegex(name)}\\b`);
|
|
225
|
-
if (regex.test(content)) {
|
|
226
|
-
if (!results.has(file)) results.set(file, /* @__PURE__ */ new Set());
|
|
227
|
-
results.get(file).add(name);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
} catch {
|
|
231
|
-
}
|
|
232
|
-
})
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
return [...results.entries()].map(([file, matches]) => ({
|
|
236
|
-
file,
|
|
237
|
-
matches: [...matches]
|
|
238
|
-
})).sort((a, b) => b.matches.length - a.matches.length);
|
|
239
|
-
}
|
|
240
|
-
async function getRelatedTests(rootDir, targetFiles) {
|
|
241
|
-
const testPatterns = [
|
|
242
|
-
"**/*.test.*",
|
|
243
|
-
"**/*.spec.*",
|
|
244
|
-
"**/*Test.kt",
|
|
245
|
-
"**/*Test.java",
|
|
246
|
-
"**/*Tests.kt",
|
|
247
|
-
"**/*Tests.java",
|
|
248
|
-
"**/test_*.py",
|
|
249
|
-
"**/*_test.py",
|
|
250
|
-
"**/*_test.go",
|
|
251
|
-
"**/*Tests.swift",
|
|
252
|
-
"**/*Test.swift",
|
|
253
|
-
"**/*_test.rs"
|
|
254
|
-
];
|
|
255
|
-
const testFiles = await fg5(testPatterns, { cwd: rootDir, ignore: IGNORE2 });
|
|
256
|
-
const results = [];
|
|
257
|
-
for (const target of targetFiles) {
|
|
258
|
-
const targetBaseName = path5.basename(target).replace(/\.[^.]+$/, "");
|
|
259
|
-
for (const testFile of testFiles) {
|
|
260
|
-
const testBaseName = path5.basename(testFile).replace(/\.[^.]+$/, "");
|
|
261
|
-
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
262
|
-
if (sourceName === targetBaseName) {
|
|
263
|
-
results.push({
|
|
264
|
-
file: testFile,
|
|
265
|
-
confidence: "exact"
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return results;
|
|
271
|
-
}
|
|
272
|
-
function escapeRegex(str) {
|
|
273
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
274
|
-
}
|
|
275
|
-
var exec7, IGNORE2, SOURCE_EXTENSIONS2;
|
|
276
|
-
var init_impact = __esm({
|
|
277
|
-
"src/impact/impact.ts"() {
|
|
278
|
-
"use strict";
|
|
279
|
-
exec7 = promisify7(execFile7);
|
|
280
|
-
IGNORE2 = [
|
|
281
|
-
"**/node_modules/**",
|
|
282
|
-
"**/dist/**",
|
|
283
|
-
"**/build/**",
|
|
284
|
-
"**/.gradle/**",
|
|
285
|
-
"**/target/**",
|
|
286
|
-
"**/.git/**",
|
|
287
|
-
"**/vendor/**",
|
|
288
|
-
"**/__pycache__/**",
|
|
289
|
-
"**/venv/**",
|
|
290
|
-
"**/.venv/**",
|
|
291
|
-
"**/generated/**"
|
|
292
|
-
];
|
|
293
|
-
SOURCE_EXTENSIONS2 = "*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,h,dart,gradle.kts,gradle}";
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
// src/mcp/server.ts
|
|
298
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
299
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
300
|
-
import { z } from "zod";
|
|
301
|
-
|
|
302
|
-
// src/mcp/tools.ts
|
|
303
|
-
import fs6 from "fs/promises";
|
|
304
|
-
import path6 from "path";
|
|
305
|
-
import { execFile as execFile8 } from "child_process";
|
|
306
|
-
import { promisify as promisify8 } from "util";
|
|
307
|
-
import fg6 from "fast-glob";
|
|
308
|
-
|
|
309
|
-
// src/analyzers/git-history.ts
|
|
310
|
-
import { execFile } from "child_process";
|
|
311
|
-
import { promisify } from "util";
|
|
312
|
-
|
|
313
|
-
// src/analyzers/base.ts
|
|
314
|
-
import fs from "fs/promises";
|
|
315
|
-
import fg from "fast-glob";
|
|
316
|
-
var BaseAnalyzer = class {
|
|
317
|
-
async findFiles(patterns, root) {
|
|
318
|
-
return fg(patterns, {
|
|
319
|
-
cwd: root,
|
|
320
|
-
ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"],
|
|
321
|
-
absolute: true
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
async readFile(filePath) {
|
|
325
|
-
return fs.readFile(filePath, "utf-8");
|
|
326
|
-
}
|
|
327
|
-
createFinding(partial) {
|
|
328
|
-
return {
|
|
329
|
-
analyzer: this.name,
|
|
330
|
-
category: partial.category,
|
|
331
|
-
confidence: partial.confidence,
|
|
332
|
-
summary: partial.summary,
|
|
333
|
-
evidence: partial.evidence ?? [],
|
|
334
|
-
ruleCandidate: partial.ruleCandidate ?? null
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
createResult(findings, gaps, startTime) {
|
|
338
|
-
return {
|
|
339
|
-
analyzer: this.name,
|
|
340
|
-
findings,
|
|
341
|
-
gaps,
|
|
342
|
-
durationMs: Date.now() - startTime
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
// src/analyzers/git-history.ts
|
|
348
|
-
var exec = promisify(execFile);
|
|
349
|
-
var GitHistoryAnalyzer = class extends BaseAnalyzer {
|
|
350
|
-
name = "git-history";
|
|
351
|
-
async analyze(context) {
|
|
352
|
-
const startTime = Date.now();
|
|
353
|
-
const findings = [];
|
|
354
|
-
const gaps = [];
|
|
355
|
-
if (!context.gitAvailable) {
|
|
356
|
-
return this.createResult([], [], startTime);
|
|
357
|
-
}
|
|
358
|
-
const [staleFindings, staleGaps] = await this.findStaleDirectories(context);
|
|
359
|
-
findings.push(...staleFindings);
|
|
360
|
-
gaps.push(...staleGaps);
|
|
361
|
-
const hotFindings = await this.findHotFiles(context);
|
|
362
|
-
findings.push(...hotFindings);
|
|
363
|
-
const commitFindings = await this.analyzeCommitPatterns(context);
|
|
364
|
-
findings.push(...commitFindings);
|
|
365
|
-
return this.createResult(findings, gaps, startTime);
|
|
366
|
-
}
|
|
367
|
-
async git(args, cwd) {
|
|
368
|
-
try {
|
|
369
|
-
const { stdout } = await exec("git", args, { cwd, maxBuffer: 1e7 });
|
|
370
|
-
return stdout.trim();
|
|
371
|
-
} catch {
|
|
372
|
-
return "";
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
async findStaleDirectories(context) {
|
|
376
|
-
const findings = [];
|
|
377
|
-
const gaps = [];
|
|
378
|
-
const output = await this.git(
|
|
379
|
-
["log", "--all", "--format=%ci", "--name-only", "--diff-filter=AMCR", "-n", "500"],
|
|
380
|
-
context.rootDir
|
|
381
|
-
);
|
|
382
|
-
if (!output) return [findings, gaps];
|
|
383
|
-
const dirLastTouch = /* @__PURE__ */ new Map();
|
|
384
|
-
let currentDate = null;
|
|
385
|
-
for (const line of output.split("\n")) {
|
|
386
|
-
if (!line) continue;
|
|
387
|
-
if (/^\d{4}-\d{2}-\d{2}/.test(line)) {
|
|
388
|
-
currentDate = new Date(line);
|
|
389
|
-
} else if (currentDate) {
|
|
390
|
-
const topDir = line.split("/")[0];
|
|
391
|
-
if (topDir && !topDir.startsWith(".") && !topDir.includes("node_modules")) {
|
|
392
|
-
const existing = dirLastTouch.get(topDir);
|
|
393
|
-
if (!existing || currentDate > existing) {
|
|
394
|
-
dirLastTouch.set(topDir, currentDate);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
const sixMonthsAgo = /* @__PURE__ */ new Date();
|
|
400
|
-
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
|
|
401
|
-
for (const [dir, lastTouch] of dirLastTouch) {
|
|
402
|
-
if (lastTouch < sixMonthsAgo) {
|
|
403
|
-
const monthsStale = Math.floor(
|
|
404
|
-
(Date.now() - lastTouch.getTime()) / (1e3 * 60 * 60 * 24 * 30)
|
|
405
|
-
);
|
|
406
|
-
findings.push(
|
|
407
|
-
this.createFinding({
|
|
408
|
-
category: "risk",
|
|
409
|
-
confidence: 0.7,
|
|
410
|
-
summary: `Directory "${dir}" hasn't been modified in ${monthsStale} months`,
|
|
411
|
-
evidence: [
|
|
412
|
-
{ filePath: dir, detail: `Last commit: ${lastTouch.toISOString().split("T")[0]}` }
|
|
413
|
-
],
|
|
414
|
-
ruleCandidate: `Do not refactor or modify files in "${dir}/" unless explicitly asked \u2014 this area has been stable for ${monthsStale} months and may be legacy code.`
|
|
415
|
-
})
|
|
416
|
-
);
|
|
417
|
-
gaps.push({
|
|
418
|
-
analyzer: this.name,
|
|
419
|
-
question: `Directory "${dir}" hasn't been touched in ${monthsStale} months. Is it deprecated, stable, or legacy?`,
|
|
420
|
-
context: `Last modified: ${lastTouch.toISOString().split("T")[0]}`,
|
|
421
|
-
answerKey: `stale-dir-${dir}`
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
return [findings, gaps];
|
|
426
|
-
}
|
|
427
|
-
async findHotFiles(context) {
|
|
428
|
-
const findings = [];
|
|
429
|
-
const output = await this.git(
|
|
430
|
-
["log", "--since=3 months ago", "--format=", "--name-only"],
|
|
431
|
-
context.rootDir
|
|
432
|
-
);
|
|
433
|
-
if (!output) return findings;
|
|
434
|
-
const fileCounts = /* @__PURE__ */ new Map();
|
|
435
|
-
for (const line of output.split("\n")) {
|
|
436
|
-
if (!line || line.startsWith(".") || line.includes("node_modules")) continue;
|
|
437
|
-
fileCounts.set(line, (fileCounts.get(line) ?? 0) + 1);
|
|
438
|
-
}
|
|
439
|
-
const sorted = [...fileCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
440
|
-
if (sorted.length > 0 && sorted[0][1] >= 5) {
|
|
441
|
-
const hotFiles = sorted.filter(([, count]) => count >= 5);
|
|
442
|
-
if (hotFiles.length > 0) {
|
|
443
|
-
findings.push(
|
|
444
|
-
this.createFinding({
|
|
445
|
-
category: "risk",
|
|
446
|
-
confidence: 0.8,
|
|
447
|
-
summary: `${hotFiles.length} files changed frequently in the last 3 months`,
|
|
448
|
-
evidence: hotFiles.map(([file, count]) => ({
|
|
449
|
-
filePath: file,
|
|
450
|
-
detail: `${count} commits`
|
|
451
|
-
})),
|
|
452
|
-
ruleCandidate: `These files change frequently and are high-risk for conflicts: ${hotFiles.map(([f]) => f).join(", ")}. Take extra care when modifying them.`
|
|
453
|
-
})
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
return findings;
|
|
458
|
-
}
|
|
459
|
-
async analyzeCommitPatterns(context) {
|
|
460
|
-
const findings = [];
|
|
461
|
-
const output = await this.git(
|
|
462
|
-
["log", "--format=%s", "-n", "100"],
|
|
463
|
-
context.rootDir
|
|
464
|
-
);
|
|
465
|
-
if (!output) return findings;
|
|
466
|
-
const messages = output.split("\n").filter(Boolean);
|
|
467
|
-
const conventionalPattern = /^(feat|fix|chore|docs|style|refactor|test|perf|ci|build|revert)(\(.+\))?:/;
|
|
468
|
-
const conventionalCount = messages.filter(
|
|
469
|
-
(m) => conventionalPattern.test(m)
|
|
470
|
-
).length;
|
|
471
|
-
const conventionalRatio = conventionalCount / messages.length;
|
|
472
|
-
if (conventionalRatio > 0.5) {
|
|
473
|
-
findings.push(
|
|
474
|
-
this.createFinding({
|
|
475
|
-
category: "convention",
|
|
476
|
-
confidence: Math.min(conventionalRatio + 0.1, 1),
|
|
477
|
-
summary: `${Math.round(conventionalRatio * 100)}% of recent commits use conventional commit format`,
|
|
478
|
-
evidence: [
|
|
479
|
-
{
|
|
480
|
-
filePath: ".git",
|
|
481
|
-
detail: `${conventionalCount} of ${messages.length} commits match`
|
|
482
|
-
}
|
|
483
|
-
],
|
|
484
|
-
ruleCandidate: "Use conventional commit format: type(scope): description (e.g., feat(auth): add login endpoint)"
|
|
485
|
-
})
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
const ticketPattern = /[A-Z]+-\d+|#\d+/;
|
|
489
|
-
const ticketCount = messages.filter((m) => ticketPattern.test(m)).length;
|
|
490
|
-
const ticketRatio = ticketCount / messages.length;
|
|
491
|
-
if (ticketRatio > 0.3) {
|
|
492
|
-
findings.push(
|
|
493
|
-
this.createFinding({
|
|
494
|
-
category: "convention",
|
|
495
|
-
confidence: ticketRatio,
|
|
496
|
-
summary: `${Math.round(ticketRatio * 100)}% of commits reference issue/ticket IDs`,
|
|
497
|
-
evidence: [
|
|
498
|
-
{
|
|
499
|
-
filePath: ".git",
|
|
500
|
-
detail: `${ticketCount} of ${messages.length} commits have ticket refs`
|
|
501
|
-
}
|
|
502
|
-
],
|
|
503
|
-
ruleCandidate: "Include issue/ticket references in commit messages when applicable."
|
|
504
|
-
})
|
|
505
|
-
);
|
|
506
|
-
}
|
|
507
|
-
return findings;
|
|
508
|
-
}
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
// src/analyzers/index.ts
|
|
512
|
-
var analyzers = [new GitHistoryAnalyzer()];
|
|
513
|
-
async function runAll(context) {
|
|
514
|
-
return Promise.all(analyzers.map((a) => a.analyze(context)));
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// src/utils/git.ts
|
|
518
|
-
import { execFile as execFile2 } from "child_process";
|
|
519
|
-
import { promisify as promisify2 } from "util";
|
|
520
|
-
var exec2 = promisify2(execFile2);
|
|
521
|
-
async function isGitRepo(dir) {
|
|
522
|
-
try {
|
|
523
|
-
await exec2("git", ["rev-parse", "--git-dir"], { cwd: dir });
|
|
524
|
-
return true;
|
|
525
|
-
} catch {
|
|
526
|
-
return false;
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// src/mcp/sampler.ts
|
|
531
|
-
import fs2 from "fs/promises";
|
|
532
|
-
import path from "path";
|
|
533
|
-
import { execFile as execFile3 } from "child_process";
|
|
534
|
-
import { promisify as promisify3 } from "util";
|
|
535
|
-
import fg2 from "fast-glob";
|
|
536
|
-
var exec3 = promisify3(execFile3);
|
|
537
|
-
var SOURCE_EXTENSIONS = [
|
|
538
|
-
"ts",
|
|
539
|
-
"tsx",
|
|
540
|
-
"js",
|
|
541
|
-
"jsx",
|
|
542
|
-
"mts",
|
|
543
|
-
"mjs",
|
|
544
|
-
"kt",
|
|
545
|
-
"kts",
|
|
546
|
-
"java",
|
|
547
|
-
"py",
|
|
548
|
-
"go",
|
|
549
|
-
"rs",
|
|
550
|
-
"swift",
|
|
551
|
-
"rb",
|
|
552
|
-
"cs",
|
|
553
|
-
"cpp",
|
|
554
|
-
"c",
|
|
555
|
-
"h",
|
|
556
|
-
"dart"
|
|
557
|
-
];
|
|
558
|
-
var CONFIG_FILES = [
|
|
559
|
-
// Build & project config
|
|
560
|
-
"package.json",
|
|
561
|
-
"tsconfig.json",
|
|
562
|
-
"build.gradle.kts",
|
|
563
|
-
"build.gradle",
|
|
564
|
-
"settings.gradle.kts",
|
|
565
|
-
"settings.gradle",
|
|
566
|
-
"Cargo.toml",
|
|
567
|
-
"go.mod",
|
|
568
|
-
"pyproject.toml",
|
|
569
|
-
"Gemfile",
|
|
570
|
-
"*.csproj",
|
|
571
|
-
// Version catalogs & dependency locks
|
|
572
|
-
"gradle/libs.versions.toml",
|
|
573
|
-
// Code quality & formatting
|
|
574
|
-
".editorconfig",
|
|
575
|
-
".eslintrc.*",
|
|
576
|
-
"eslint.config.*",
|
|
577
|
-
".prettierrc",
|
|
578
|
-
"rustfmt.toml",
|
|
579
|
-
".swiftlint.yml",
|
|
580
|
-
// CI/CD
|
|
581
|
-
".github/workflows/*.yml",
|
|
582
|
-
".gitlab-ci.yml",
|
|
583
|
-
"Jenkinsfile",
|
|
584
|
-
// Containerization
|
|
585
|
-
"Dockerfile",
|
|
586
|
-
"docker-compose.yml",
|
|
587
|
-
"docker-compose.yaml"
|
|
588
|
-
];
|
|
589
|
-
var ENTRY_POINT_PATTERNS = [
|
|
590
|
-
"src/main.*",
|
|
591
|
-
"src/index.*",
|
|
592
|
-
"src/app.*",
|
|
593
|
-
"main.*",
|
|
594
|
-
"index.*",
|
|
595
|
-
"app.*",
|
|
596
|
-
"App.*",
|
|
597
|
-
"**/Main.kt",
|
|
598
|
-
"**/Application.kt",
|
|
599
|
-
"**/main.py",
|
|
600
|
-
"**/main.go",
|
|
601
|
-
"**/main.rs",
|
|
602
|
-
"**/lib.rs",
|
|
603
|
-
"**/Program.cs"
|
|
604
|
-
];
|
|
605
|
-
var ARCHITECTURAL_PATTERNS = [
|
|
606
|
-
// State/data flow
|
|
607
|
-
{ glob: "**/*ViewModel.*", category: "state", reason: "viewmodel (state management)" },
|
|
608
|
-
{ glob: "**/*Store.*", category: "state", reason: "store (state management)" },
|
|
609
|
-
{ glob: "**/*Reducer.*", category: "state", reason: "reducer (state management)" },
|
|
610
|
-
// Data layer — interface
|
|
611
|
-
{ glob: "**/*Repository.*", category: "data-interface", reason: "repository interface (data layer contract)" },
|
|
612
|
-
{ glob: "**/*Dao.*", category: "data-interface", reason: "DAO (data access)" },
|
|
613
|
-
{ glob: "**/*DataSource.*", category: "data-interface", reason: "data source" },
|
|
614
|
-
// Data layer — implementation (where actual patterns live: mappers, retry, IO dispatchers)
|
|
615
|
-
{ glob: "**/*RepositoryImpl.*", category: "data-impl", reason: "repository implementation (data layer patterns)" },
|
|
616
|
-
{ glob: "**/*ServiceImpl.*", category: "data-impl", reason: "service implementation" },
|
|
617
|
-
{ glob: "**/*Impl.*", category: "data-impl", reason: "implementation (concrete patterns)" },
|
|
618
|
-
// Data transformation
|
|
619
|
-
{ glob: "**/*Mapper.*", category: "transform", reason: "mapper (data transformation)" },
|
|
620
|
-
{ glob: "**/*Converter.*", category: "transform", reason: "converter (data transformation)" },
|
|
621
|
-
{ glob: "**/*Adapter.*", category: "transform", reason: "adapter (interface adaptation)" },
|
|
622
|
-
// Dependency injection / wiring
|
|
623
|
-
{ glob: "**/*Module.*", category: "di", reason: "module (DI/wiring)" },
|
|
624
|
-
{ glob: "**/*Provider.*", category: "di", reason: "provider (DI/wiring)" },
|
|
625
|
-
{ glob: "**/*Container.*", category: "di", reason: "container (DI/wiring)" },
|
|
626
|
-
{ glob: "**/*Factory.*", category: "di", reason: "factory (object creation)" },
|
|
627
|
-
// API / network
|
|
628
|
-
{ glob: "**/*Service.*", category: "api", reason: "service (business/API layer)" },
|
|
629
|
-
{ glob: "**/*Client.*", category: "api", reason: "client (API/network layer)" },
|
|
630
|
-
{ glob: "**/*Api.*", category: "api", reason: "API interface definition" },
|
|
631
|
-
// Interface contracts / protocols
|
|
632
|
-
{ glob: "**/*Interface.*", category: "contract", reason: "interface definition" },
|
|
633
|
-
{ glob: "**/*Protocol.*", category: "contract", reason: "protocol definition" },
|
|
634
|
-
{ glob: "**/*Trait.*", category: "contract", reason: "trait definition" },
|
|
635
|
-
// Routing / navigation
|
|
636
|
-
{ glob: "**/*Router.*", category: "routing", reason: "router (navigation/routing)" },
|
|
637
|
-
{ glob: "**/*Route.*", category: "routing", reason: "route definition" },
|
|
638
|
-
{ glob: "**/*NavHost.*", category: "routing", reason: "navigation host" },
|
|
639
|
-
{ glob: "**/*Controller.*", category: "routing", reason: "controller (request handling)" },
|
|
640
|
-
{ glob: "**/*Handler.*", category: "routing", reason: "handler (request handling)" },
|
|
641
|
-
// Middleware / interceptors
|
|
642
|
-
{ glob: "**/*Middleware.*", category: "middleware", reason: "middleware (request pipeline)" },
|
|
643
|
-
{ glob: "**/*Interceptor.*", category: "middleware", reason: "interceptor (cross-cutting)" },
|
|
644
|
-
{ glob: "**/*Plugin.*", category: "middleware", reason: "plugin (extensibility)" },
|
|
645
|
-
// Models / types
|
|
646
|
-
{ glob: "**/*Model.*", category: "model", reason: "model (domain types)" },
|
|
647
|
-
{ glob: "**/*Entity.*", category: "model", reason: "entity (persistence types)" },
|
|
648
|
-
{ glob: "**/*Dto.*", category: "model", reason: "DTO (data transfer types)" },
|
|
649
|
-
{ glob: "**/*Schema.*", category: "model", reason: "schema (data validation)" },
|
|
650
|
-
// Use cases / commands
|
|
651
|
-
{ glob: "**/*UseCase.*", category: "usecase", reason: "use case (business logic)" },
|
|
652
|
-
{ glob: "**/*Interactor.*", category: "usecase", reason: "interactor (business logic)" },
|
|
653
|
-
{ glob: "**/*Command.*", category: "usecase", reason: "command (CQRS pattern)" }
|
|
654
|
-
];
|
|
655
|
-
var IGNORE_PATTERNS = [
|
|
656
|
-
"**/node_modules/**",
|
|
657
|
-
"**/dist/**",
|
|
658
|
-
"**/build/**",
|
|
659
|
-
"**/.gradle/**",
|
|
660
|
-
"**/target/**",
|
|
661
|
-
"**/.git/**",
|
|
662
|
-
"**/vendor/**",
|
|
663
|
-
"**/__pycache__/**",
|
|
664
|
-
"**/venv/**",
|
|
665
|
-
"**/.venv/**",
|
|
666
|
-
"**/*.min.*",
|
|
667
|
-
"**/*.map",
|
|
668
|
-
"**/package-lock.json",
|
|
669
|
-
"**/yarn.lock",
|
|
670
|
-
"**/pnpm-lock.yaml",
|
|
671
|
-
"**/*.lock",
|
|
672
|
-
"**/*.generated.*",
|
|
673
|
-
"**/generated/**",
|
|
674
|
-
"**/R.java",
|
|
675
|
-
"**/BuildConfig.java"
|
|
676
|
-
];
|
|
677
|
-
var PREVIEW_LINES = 60;
|
|
678
|
-
async function loadProjectConfig(rootDir) {
|
|
679
|
-
try {
|
|
680
|
-
const raw = await fs2.readFile(
|
|
681
|
-
path.join(rootDir, ".mason", "config.json"),
|
|
682
|
-
"utf-8"
|
|
683
|
-
);
|
|
684
|
-
return JSON.parse(raw);
|
|
685
|
-
} catch {
|
|
686
|
-
return {};
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
async function getTrackedFiles(rootDir) {
|
|
690
|
-
try {
|
|
691
|
-
const { stdout } = await exec3("git", ["ls-files", "--cached", "--others", "--exclude-standard"], {
|
|
692
|
-
cwd: rootDir,
|
|
693
|
-
maxBuffer: 1e7
|
|
694
|
-
});
|
|
695
|
-
return new Set(stdout.trim().split("\n").filter(Boolean));
|
|
696
|
-
} catch {
|
|
697
|
-
return null;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
async function sampleFiles(rootDir, maxFiles = 25) {
|
|
701
|
-
const selected = /* @__PURE__ */ new Map();
|
|
702
|
-
const projectConfig = await loadProjectConfig(rootDir);
|
|
703
|
-
const ignorePatterns = [...IGNORE_PATTERNS, ...projectConfig.ignore ?? []];
|
|
704
|
-
const trackedFiles = await getTrackedFiles(rootDir);
|
|
705
|
-
for (const filePath of projectConfig.alwaysInclude ?? []) {
|
|
706
|
-
if (selected.size >= maxFiles) break;
|
|
707
|
-
const resolvedPath = path.resolve(rootDir, filePath);
|
|
708
|
-
if (!resolvedPath.startsWith(path.resolve(rootDir))) continue;
|
|
709
|
-
selected.set(filePath, "always-include (project config)");
|
|
710
|
-
}
|
|
711
|
-
let configCount = 0;
|
|
712
|
-
for (const pattern of CONFIG_FILES) {
|
|
713
|
-
if (configCount >= 5) break;
|
|
714
|
-
const matches = await fg2(pattern, {
|
|
715
|
-
cwd: rootDir,
|
|
716
|
-
ignore: ignorePatterns,
|
|
717
|
-
deep: 3
|
|
718
|
-
});
|
|
719
|
-
for (const match of matches) {
|
|
720
|
-
if (configCount >= 5 || selected.size >= maxFiles) break;
|
|
721
|
-
selected.set(match, "config file");
|
|
722
|
-
configCount++;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
const moduleBuildPatterns = [
|
|
726
|
-
// Gradle
|
|
727
|
-
"**/build.gradle.kts",
|
|
728
|
-
"**/build.gradle",
|
|
729
|
-
// Cargo workspace members
|
|
730
|
-
"**/Cargo.toml",
|
|
731
|
-
// Node workspaces
|
|
732
|
-
"**/package.json",
|
|
733
|
-
// Go sub-modules
|
|
734
|
-
"**/go.mod"
|
|
735
|
-
];
|
|
736
|
-
let moduleBuildCount = 0;
|
|
737
|
-
for (const pattern of moduleBuildPatterns) {
|
|
738
|
-
const matches = await fg2(pattern, {
|
|
739
|
-
cwd: rootDir,
|
|
740
|
-
ignore: ignorePatterns,
|
|
741
|
-
deep: 4
|
|
742
|
-
});
|
|
743
|
-
const subMatches = matches.filter((m) => m.includes("/"));
|
|
744
|
-
for (const match of subMatches) {
|
|
745
|
-
if (moduleBuildCount >= 4 || selected.size >= maxFiles) break;
|
|
746
|
-
if (!selected.has(match)) {
|
|
747
|
-
selected.set(match, "module build file (reveals dependency graph)");
|
|
748
|
-
moduleBuildCount++;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
if (moduleBuildCount >= 4) break;
|
|
752
|
-
}
|
|
753
|
-
let entryCount = 0;
|
|
754
|
-
for (const pattern of ENTRY_POINT_PATTERNS) {
|
|
755
|
-
if (entryCount >= 2) break;
|
|
756
|
-
const matches = await fg2(pattern, {
|
|
757
|
-
cwd: rootDir,
|
|
758
|
-
ignore: ignorePatterns,
|
|
759
|
-
deep: 5
|
|
760
|
-
});
|
|
761
|
-
for (const match of matches) {
|
|
762
|
-
if (entryCount >= 2 || selected.size >= maxFiles) break;
|
|
763
|
-
if (!selected.has(match)) {
|
|
764
|
-
selected.set(match, "entry point");
|
|
765
|
-
entryCount++;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
try {
|
|
770
|
-
const { stdout } = await exec3(
|
|
771
|
-
"git",
|
|
772
|
-
["log", "--since=3 months ago", "--format=", "--name-only"],
|
|
773
|
-
{ cwd: rootDir, maxBuffer: 5e6 }
|
|
774
|
-
);
|
|
775
|
-
const fileCounts = /* @__PURE__ */ new Map();
|
|
776
|
-
for (const line of stdout.split("\n")) {
|
|
777
|
-
if (!line) continue;
|
|
778
|
-
if (line.includes("node_modules") || line.includes("/build/") || line.includes(".gradle") || line.includes("/generated/"))
|
|
779
|
-
continue;
|
|
780
|
-
const ext = path.extname(line).slice(1);
|
|
781
|
-
if (!SOURCE_EXTENSIONS.includes(ext)) continue;
|
|
782
|
-
fileCounts.set(line, (fileCounts.get(line) ?? 0) + 1);
|
|
783
|
-
}
|
|
784
|
-
const hotFiles = [...fileCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
785
|
-
for (const [file, count] of hotFiles) {
|
|
786
|
-
if (selected.size >= maxFiles) break;
|
|
787
|
-
if (!selected.has(file)) {
|
|
788
|
-
selected.set(file, `frequently changed (${count} commits in 3 months)`);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
} catch {
|
|
792
|
-
}
|
|
793
|
-
const seenCategories = /* @__PURE__ */ new Set();
|
|
794
|
-
let patternCount = 0;
|
|
795
|
-
for (const pattern of ARCHITECTURAL_PATTERNS) {
|
|
796
|
-
if (patternCount >= 8 || selected.size >= maxFiles) break;
|
|
797
|
-
if (seenCategories.has(pattern.category)) continue;
|
|
798
|
-
const matches = await fg2(pattern.glob, {
|
|
799
|
-
cwd: rootDir,
|
|
800
|
-
ignore: ignorePatterns
|
|
801
|
-
});
|
|
802
|
-
if (matches.length > 0) {
|
|
803
|
-
for (const match of matches) {
|
|
804
|
-
if (!selected.has(match)) {
|
|
805
|
-
selected.set(match, pattern.reason);
|
|
806
|
-
seenCategories.add(pattern.category);
|
|
807
|
-
patternCount++;
|
|
808
|
-
break;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
for (const customGlob of projectConfig.patterns ?? []) {
|
|
814
|
-
if (selected.size >= maxFiles) break;
|
|
815
|
-
const matches = await fg2(customGlob, {
|
|
816
|
-
cwd: rootDir,
|
|
817
|
-
ignore: ignorePatterns
|
|
818
|
-
});
|
|
819
|
-
for (const match of matches) {
|
|
820
|
-
if (selected.size >= maxFiles) break;
|
|
821
|
-
if (!selected.has(match)) {
|
|
822
|
-
selected.set(match, "custom pattern (project config)");
|
|
823
|
-
break;
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
const testPatternGroups = [
|
|
828
|
-
// JS/TS tests
|
|
829
|
-
{ patterns: ["**/*.test.*", "**/*.spec.*"], label: "JS/TS test" },
|
|
830
|
-
// JVM tests
|
|
831
|
-
{ patterns: ["**/*Test.kt", "**/*Test.java"], label: "JVM test" },
|
|
832
|
-
// Python tests
|
|
833
|
-
{ patterns: ["**/test_*.py", "**/*_test.py"], label: "Python test" },
|
|
834
|
-
// Go tests
|
|
835
|
-
{ patterns: ["**/*_test.go"], label: "Go test" },
|
|
836
|
-
// Swift tests
|
|
837
|
-
{ patterns: ["**/*Tests.swift", "**/*Test.swift"], label: "Swift test" },
|
|
838
|
-
// Rust tests
|
|
839
|
-
{ patterns: ["**/*_test.rs"], label: "Rust test" }
|
|
840
|
-
];
|
|
841
|
-
let testCount = 0;
|
|
842
|
-
for (const group of testPatternGroups) {
|
|
843
|
-
if (testCount >= 3 || selected.size >= maxFiles) break;
|
|
844
|
-
const testFiles = await fg2(group.patterns, {
|
|
845
|
-
cwd: rootDir,
|
|
846
|
-
ignore: ignorePatterns
|
|
847
|
-
});
|
|
848
|
-
if (testFiles.length > 0) {
|
|
849
|
-
for (const file of testFiles) {
|
|
850
|
-
if (!selected.has(file)) {
|
|
851
|
-
selected.set(file, `test example (${group.label})`);
|
|
852
|
-
testCount++;
|
|
853
|
-
break;
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
const sourceGlobs = SOURCE_EXTENSIONS.map((ext) => `**/*.${ext}`);
|
|
859
|
-
const allSourceFiles = await fg2(sourceGlobs, {
|
|
860
|
-
cwd: rootDir,
|
|
861
|
-
ignore: ignorePatterns
|
|
862
|
-
});
|
|
863
|
-
const dirRepresentatives = /* @__PURE__ */ new Map();
|
|
864
|
-
const boringFiles = /\.(gradle|gradle\.kts|json|toml|yaml|yml|xml|properties)$/;
|
|
865
|
-
for (const file of allSourceFiles) {
|
|
866
|
-
const topDir = file.split("/")[0];
|
|
867
|
-
if (!dirRepresentatives.has(topDir) && !boringFiles.test(file)) {
|
|
868
|
-
dirRepresentatives.set(topDir, file);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
for (const [, file] of dirRepresentatives) {
|
|
872
|
-
if (selected.size >= maxFiles) break;
|
|
873
|
-
if (!selected.has(file)) {
|
|
874
|
-
selected.set(file, "directory representative");
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
const results = [];
|
|
878
|
-
for (const [filePath, reason] of selected) {
|
|
879
|
-
try {
|
|
880
|
-
const fullPath = path.resolve(rootDir, filePath);
|
|
881
|
-
if (!fullPath.startsWith(path.resolve(rootDir))) continue;
|
|
882
|
-
if (isSensitiveFile(filePath)) continue;
|
|
883
|
-
if (trackedFiles && !trackedFiles.has(filePath)) continue;
|
|
884
|
-
const stat = await fs2.stat(fullPath);
|
|
885
|
-
if (stat.size > 1e5) continue;
|
|
886
|
-
const content = await fs2.readFile(fullPath, "utf-8");
|
|
887
|
-
const lines = content.split("\n");
|
|
888
|
-
const preview = lines.slice(0, PREVIEW_LINES).join("\n");
|
|
889
|
-
results.push({
|
|
890
|
-
path: filePath,
|
|
891
|
-
preview,
|
|
892
|
-
totalLines: lines.length,
|
|
893
|
-
sizeBytes: stat.size,
|
|
894
|
-
reason
|
|
895
|
-
});
|
|
896
|
-
} catch {
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
return results;
|
|
900
|
-
}
|
|
901
|
-
var SENSITIVE_PATTERNS = [
|
|
902
|
-
/^\.env$/,
|
|
903
|
-
/^\.env\./,
|
|
904
|
-
/\.pem$/,
|
|
905
|
-
/\.key$/,
|
|
906
|
-
/\.p12$/,
|
|
907
|
-
/\.pfx$/,
|
|
908
|
-
/\.jks$/,
|
|
909
|
-
/id_rsa/,
|
|
910
|
-
/id_ed25519/,
|
|
911
|
-
/credentials\./,
|
|
912
|
-
/secret/i,
|
|
913
|
-
/\.keystore$/,
|
|
914
|
-
/local\.properties$/
|
|
915
|
-
];
|
|
916
|
-
function isSensitiveFile(filePath) {
|
|
917
|
-
const basename = path.basename(filePath);
|
|
918
|
-
return SENSITIVE_PATTERNS.some((p) => p.test(basename));
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
// src/snapshot/snapshot.ts
|
|
922
|
-
import fs4 from "fs/promises";
|
|
923
|
-
import path4 from "path";
|
|
924
|
-
import { execFile as execFile6 } from "child_process";
|
|
925
|
-
import { promisify as promisify6 } from "util";
|
|
926
|
-
import fg4 from "fast-glob";
|
|
927
|
-
init_test_map();
|
|
928
|
-
|
|
929
|
-
// src/llm/providers.ts
|
|
930
|
-
import { execFile as execFile5, spawn } from "child_process";
|
|
931
|
-
import { promisify as promisify5 } from "util";
|
|
932
|
-
|
|
933
|
-
// src/llm/config.ts
|
|
934
|
-
import fs3 from "fs/promises";
|
|
935
|
-
import path3 from "path";
|
|
936
|
-
import os from "os";
|
|
937
|
-
import { execFile as execFile4 } from "child_process";
|
|
938
|
-
import { promisify as promisify4 } from "util";
|
|
939
|
-
var exec4 = promisify4(execFile4);
|
|
940
|
-
var CONFIG_DIR = path3.join(os.homedir(), ".mason");
|
|
941
|
-
var CONFIG_FILE = path3.join(CONFIG_DIR, "config.json");
|
|
942
|
-
|
|
943
|
-
// src/llm/providers.ts
|
|
944
|
-
var exec5 = promisify5(execFile5);
|
|
945
|
-
|
|
946
|
-
// src/snapshot/snapshot.ts
|
|
947
|
-
var exec6 = promisify6(execFile6);
|
|
948
|
-
function snapshotDir(rootDir) {
|
|
949
|
-
return path4.join(rootDir, ".mason");
|
|
950
|
-
}
|
|
951
|
-
function snapshotPath(rootDir) {
|
|
952
|
-
return path4.join(snapshotDir(rootDir), "snapshot.json");
|
|
953
|
-
}
|
|
954
|
-
async function loadSnapshot(rootDir) {
|
|
955
|
-
try {
|
|
956
|
-
const raw = await fs4.readFile(snapshotPath(rootDir), "utf-8");
|
|
957
|
-
const parsed = JSON.parse(raw);
|
|
958
|
-
if (parsed.version !== 2) return null;
|
|
959
|
-
return parsed;
|
|
960
|
-
} catch {
|
|
961
|
-
return null;
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
async function saveSnapshot(rootDir, snapshot) {
|
|
965
|
-
await fs4.mkdir(snapshotDir(rootDir), { recursive: true });
|
|
966
|
-
await fs4.writeFile(
|
|
967
|
-
snapshotPath(rootDir),
|
|
968
|
-
JSON.stringify(snapshot, null, 2),
|
|
969
|
-
"utf-8"
|
|
970
|
-
);
|
|
971
|
-
}
|
|
972
|
-
async function getCurrentGitHash(rootDir) {
|
|
973
|
-
try {
|
|
974
|
-
const { stdout } = await exec6("git", ["rev-parse", "HEAD"], {
|
|
975
|
-
cwd: rootDir
|
|
976
|
-
});
|
|
977
|
-
return stdout.trim();
|
|
978
|
-
} catch {
|
|
979
|
-
return "unknown";
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// src/mcp/tools.ts
|
|
984
|
-
var exec8 = promisify8(execFile8);
|
|
985
|
-
var IGNORE3 = [
|
|
986
|
-
"**/node_modules/**",
|
|
987
|
-
"**/dist/**",
|
|
988
|
-
"**/build/**",
|
|
989
|
-
"**/.gradle/**",
|
|
990
|
-
"**/target/**",
|
|
991
|
-
"**/.git/**",
|
|
992
|
-
"**/vendor/**",
|
|
993
|
-
"**/__pycache__/**",
|
|
994
|
-
"**/venv/**",
|
|
995
|
-
"**/.venv/**",
|
|
996
|
-
"**/*.min.*",
|
|
997
|
-
"**/*.map"
|
|
998
|
-
];
|
|
999
|
-
async function buildContext(dir) {
|
|
1000
|
-
return {
|
|
1001
|
-
rootDir: dir,
|
|
1002
|
-
gitAvailable: await isGitRepo(dir)
|
|
1003
|
-
};
|
|
1004
|
-
}
|
|
1005
|
-
async function analyzeProject(dir) {
|
|
1006
|
-
const rootDir = path6.resolve(dir);
|
|
1007
|
-
const context = await buildContext(rootDir);
|
|
1008
|
-
const results = await runAll(context);
|
|
1009
|
-
const projectSnapshot = await detectProjectSnapshot(rootDir);
|
|
1010
|
-
const output = {
|
|
1011
|
-
project: projectSnapshot,
|
|
1012
|
-
analyzers: results.map((r) => ({
|
|
1013
|
-
name: r.analyzer,
|
|
1014
|
-
durationMs: r.durationMs,
|
|
1015
|
-
findings: r.findings.map((f) => ({
|
|
1016
|
-
category: f.category,
|
|
1017
|
-
confidence: f.confidence,
|
|
1018
|
-
summary: f.summary,
|
|
1019
|
-
evidence: f.evidence,
|
|
1020
|
-
suggestedRule: f.ruleCandidate
|
|
1021
|
-
})),
|
|
1022
|
-
gaps: r.gaps.map((g) => ({
|
|
1023
|
-
question: g.question,
|
|
1024
|
-
context: g.context
|
|
1025
|
-
}))
|
|
1026
|
-
}))
|
|
1027
|
-
};
|
|
1028
|
-
return JSON.stringify(output, null, 2);
|
|
1029
|
-
}
|
|
1030
|
-
async function detectProjectSnapshot(rootDir) {
|
|
1031
|
-
const buildFiles = [
|
|
1032
|
-
"package.json",
|
|
1033
|
-
"tsconfig.json",
|
|
1034
|
-
"build.gradle.kts",
|
|
1035
|
-
"build.gradle",
|
|
1036
|
-
"settings.gradle.kts",
|
|
1037
|
-
"settings.gradle",
|
|
1038
|
-
"gradle/libs.versions.toml",
|
|
1039
|
-
"Cargo.toml",
|
|
1040
|
-
"go.mod",
|
|
1041
|
-
"go.sum",
|
|
1042
|
-
"pyproject.toml",
|
|
1043
|
-
"setup.py",
|
|
1044
|
-
"requirements.txt",
|
|
1045
|
-
"Pipfile",
|
|
1046
|
-
"Gemfile",
|
|
1047
|
-
"Package.swift",
|
|
1048
|
-
"Makefile",
|
|
1049
|
-
"CMakeLists.txt",
|
|
1050
|
-
"Dockerfile",
|
|
1051
|
-
"docker-compose.yml",
|
|
1052
|
-
"docker-compose.yaml",
|
|
1053
|
-
".github/workflows",
|
|
1054
|
-
".gitlab-ci.yml",
|
|
1055
|
-
"Jenkinsfile"
|
|
1056
|
-
];
|
|
1057
|
-
const present = [];
|
|
1058
|
-
for (const file of buildFiles) {
|
|
1059
|
-
try {
|
|
1060
|
-
await fs6.access(path6.join(rootDir, file));
|
|
1061
|
-
present.push(file);
|
|
1062
|
-
} catch {
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
const testDirs = [
|
|
1066
|
-
"test",
|
|
1067
|
-
"tests",
|
|
1068
|
-
"__tests__",
|
|
1069
|
-
"spec",
|
|
1070
|
-
"src/test",
|
|
1071
|
-
"src/tests",
|
|
1072
|
-
"**/src/test",
|
|
1073
|
-
"**/src/androidTest",
|
|
1074
|
-
"**/src/iosTest"
|
|
1075
|
-
];
|
|
1076
|
-
const testInfo = {};
|
|
1077
|
-
for (const pattern of testDirs) {
|
|
1078
|
-
const files = await fg6(`${pattern}/**/*`, {
|
|
1079
|
-
cwd: rootDir,
|
|
1080
|
-
ignore: IGNORE3,
|
|
1081
|
-
onlyFiles: true
|
|
1082
|
-
});
|
|
1083
|
-
if (files.length > 0) {
|
|
1084
|
-
testInfo[pattern] = files.length;
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
const testFilePatterns = [
|
|
1088
|
-
{ pattern: "**/*.test.*", label: "*.test.*" },
|
|
1089
|
-
{ pattern: "**/*.spec.*", label: "*.spec.*" },
|
|
1090
|
-
{ pattern: "**/*Test.kt", label: "*Test.kt" },
|
|
1091
|
-
{ pattern: "**/*Test.java", label: "*Test.java" },
|
|
1092
|
-
{ pattern: "**/test_*.py", label: "test_*.py" },
|
|
1093
|
-
{ pattern: "**/*_test.go", label: "*_test.go" },
|
|
1094
|
-
{ pattern: "**/*Tests.swift", label: "*Tests.swift" },
|
|
1095
|
-
{ pattern: "**/*_test.rs", label: "*_test.rs" }
|
|
1096
|
-
];
|
|
1097
|
-
for (const { pattern, label } of testFilePatterns) {
|
|
1098
|
-
const files = await fg6(pattern, { cwd: rootDir, ignore: IGNORE3 });
|
|
1099
|
-
if (files.length > 0) {
|
|
1100
|
-
testInfo[label] = files.length;
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
const sourceFiles = await fg6("**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}", {
|
|
1104
|
-
cwd: rootDir,
|
|
1105
|
-
ignore: IGNORE3
|
|
1106
|
-
});
|
|
1107
|
-
const fileCounts = {};
|
|
1108
|
-
for (const file of sourceFiles) {
|
|
1109
|
-
const ext = path6.extname(file).slice(1);
|
|
1110
|
-
fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
|
|
1111
|
-
}
|
|
1112
|
-
return {
|
|
1113
|
-
configFilesPresent: present,
|
|
1114
|
-
sourceFileCounts: fileCounts,
|
|
1115
|
-
totalSourceFiles: sourceFiles.length,
|
|
1116
|
-
testInfo: Object.keys(testInfo).length > 0 ? testInfo : void 0
|
|
1117
|
-
};
|
|
1118
|
-
}
|
|
1119
|
-
async function getCodeSamples(dir, count = 15) {
|
|
1120
|
-
const rootDir = path6.resolve(dir);
|
|
1121
|
-
const samples = await sampleFiles(rootDir, count);
|
|
1122
|
-
const output = {
|
|
1123
|
-
note: "These are previews (first ~60 lines). Use get_file_content to read the full file if needed.",
|
|
1124
|
-
files: samples.map((s) => ({
|
|
1125
|
-
path: s.path,
|
|
1126
|
-
reason: s.reason,
|
|
1127
|
-
totalLines: s.totalLines,
|
|
1128
|
-
sizeBytes: s.sizeBytes,
|
|
1129
|
-
preview: s.preview
|
|
1130
|
-
}))
|
|
1131
|
-
};
|
|
1132
|
-
return JSON.stringify(output, null, 2);
|
|
1133
|
-
}
|
|
1134
|
-
async function getProjectStructure(dir) {
|
|
1135
|
-
const rootDir = path6.resolve(dir);
|
|
1136
|
-
const allFiles = await fg6("**/*", {
|
|
1137
|
-
cwd: rootDir,
|
|
1138
|
-
ignore: IGNORE3,
|
|
1139
|
-
onlyFiles: true
|
|
1140
|
-
});
|
|
1141
|
-
const dirInfo = /* @__PURE__ */ new Map();
|
|
1142
|
-
for (const file of allFiles) {
|
|
1143
|
-
const parts = file.split("/");
|
|
1144
|
-
for (let depth = 1; depth <= Math.min(parts.length, 2); depth++) {
|
|
1145
|
-
const dirPath = parts.slice(0, depth).join("/");
|
|
1146
|
-
if (!dirInfo.has(dirPath)) {
|
|
1147
|
-
dirInfo.set(dirPath, { fileCount: 0, extensions: /* @__PURE__ */ new Map() });
|
|
1148
|
-
}
|
|
1149
|
-
const info = dirInfo.get(dirPath);
|
|
1150
|
-
info.fileCount++;
|
|
1151
|
-
const ext = path6.extname(file).slice(1);
|
|
1152
|
-
if (ext) {
|
|
1153
|
-
info.extensions.set(ext, (info.extensions.get(ext) ?? 0) + 1);
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
const directories = [...dirInfo.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([dirPath, info]) => {
|
|
1158
|
-
const extensions = {};
|
|
1159
|
-
for (const [ext, count] of info.extensions) {
|
|
1160
|
-
extensions[ext] = count;
|
|
1161
|
-
}
|
|
1162
|
-
return { path: dirPath, fileCount: info.fileCount, extensions };
|
|
1163
|
-
});
|
|
1164
|
-
const topLevelFiles = allFiles.filter((f) => !f.includes("/"));
|
|
1165
|
-
const output = {
|
|
1166
|
-
totalFiles: allFiles.length,
|
|
1167
|
-
topLevelFiles,
|
|
1168
|
-
directories
|
|
1169
|
-
};
|
|
1170
|
-
return JSON.stringify(output, null, 2);
|
|
1171
|
-
}
|
|
1172
|
-
async function getTestMap(dir) {
|
|
1173
|
-
const { buildTestMap: buildTestMap2 } = await Promise.resolve().then(() => (init_test_map(), test_map_exports));
|
|
1174
|
-
const result = await buildTestMap2(dir);
|
|
1175
|
-
return JSON.stringify(result, null, 2);
|
|
1176
|
-
}
|
|
1177
|
-
async function getSnapshot(dir) {
|
|
1178
|
-
const rootDir = path6.resolve(dir);
|
|
1179
|
-
const snapshot = await loadSnapshot(rootDir);
|
|
1180
|
-
if (!snapshot) {
|
|
1181
|
-
return JSON.stringify({
|
|
1182
|
-
exists: false,
|
|
1183
|
-
message: "No concept map found. Run 'mason snapshot' to create one, or call save_snapshot with features and flows."
|
|
1184
|
-
});
|
|
1185
|
-
}
|
|
1186
|
-
const currentHash = await getCurrentGitHash(rootDir);
|
|
1187
|
-
const isStale = snapshot.gitHash !== currentHash && snapshot.gitHash !== "unknown";
|
|
1188
|
-
const seenFiles = /* @__PURE__ */ new Set();
|
|
1189
|
-
const compactFeatures = {};
|
|
1190
|
-
for (const [name, feat] of Object.entries(snapshot.features)) {
|
|
1191
|
-
const unique = feat.files.filter((f) => !seenFiles.has(f));
|
|
1192
|
-
if (unique.length === 0) continue;
|
|
1193
|
-
for (const f of unique) seenFiles.add(f);
|
|
1194
|
-
const entry = { files: unique };
|
|
1195
|
-
if (feat.tests && feat.tests.length > 0) {
|
|
1196
|
-
entry.tests = feat.tests;
|
|
1197
|
-
}
|
|
1198
|
-
compactFeatures[name] = entry;
|
|
1199
|
-
}
|
|
1200
|
-
const compactFlows = {};
|
|
1201
|
-
for (const [name, flow] of Object.entries(snapshot.flows)) {
|
|
1202
|
-
compactFlows[name] = flow.chain;
|
|
1203
|
-
}
|
|
1204
|
-
const output = {
|
|
1205
|
-
exists: true,
|
|
1206
|
-
updatedAt: snapshot.updatedAt,
|
|
1207
|
-
features: compactFeatures,
|
|
1208
|
-
flows: compactFlows,
|
|
1209
|
-
stale: isStale
|
|
1210
|
-
};
|
|
1211
|
-
if (isStale) {
|
|
1212
|
-
output.message = "Snapshot is behind HEAD. Run 'mason snapshot-update' or call save_snapshot to refresh.";
|
|
1213
|
-
}
|
|
1214
|
-
return JSON.stringify(output);
|
|
1215
|
-
}
|
|
1216
|
-
async function fullAnalysis(dir) {
|
|
1217
|
-
const rootDir = path6.resolve(dir);
|
|
1218
|
-
const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
|
|
1219
|
-
analyzeProject(dir),
|
|
1220
|
-
getProjectStructure(dir),
|
|
1221
|
-
getCodeSamples(dir, 25),
|
|
1222
|
-
getTestMap(dir),
|
|
1223
|
-
loadSnapshot(rootDir)
|
|
1224
|
-
]);
|
|
1225
|
-
const output = {
|
|
1226
|
-
note: "Full project analysis. Code samples are previews (~60 lines). Use get_file_content to read any file in full.",
|
|
1227
|
-
analysis: JSON.parse(analysis),
|
|
1228
|
-
structure: JSON.parse(structure),
|
|
1229
|
-
codeSamples: JSON.parse(samples),
|
|
1230
|
-
testMap: JSON.parse(testMap)
|
|
1231
|
-
};
|
|
1232
|
-
if (snapshot) {
|
|
1233
|
-
output.conceptMap = {
|
|
1234
|
-
updatedAt: snapshot.updatedAt,
|
|
1235
|
-
features: snapshot.features,
|
|
1236
|
-
flows: snapshot.flows
|
|
1237
|
-
};
|
|
1238
|
-
output.note = "Full project analysis with concept map. The concept map shows which files implement each feature and how data flows through them. Use it to jump straight to relevant files instead of exploring. Use get_file_content to read specific files.";
|
|
1239
|
-
}
|
|
1240
|
-
return JSON.stringify(output, null, 2);
|
|
1241
|
-
}
|
|
1242
|
-
function sanitizePaths(rootDir, files) {
|
|
1243
|
-
return files.filter((f) => {
|
|
1244
|
-
const resolved = path6.resolve(rootDir, f);
|
|
1245
|
-
return resolved.startsWith(rootDir) && !f.startsWith("/") && !f.includes("..");
|
|
1246
|
-
});
|
|
1247
|
-
}
|
|
1248
|
-
async function saveSnapshotData(dir, features, flows) {
|
|
1249
|
-
const rootDir = path6.resolve(dir);
|
|
1250
|
-
const gitHash = await getCurrentGitHash(rootDir);
|
|
1251
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1252
|
-
for (const feat of Object.values(features)) {
|
|
1253
|
-
feat.files = sanitizePaths(rootDir, feat.files);
|
|
1254
|
-
if (feat.tests) feat.tests = sanitizePaths(rootDir, feat.tests);
|
|
1255
|
-
}
|
|
1256
|
-
for (const flow of Object.values(flows)) {
|
|
1257
|
-
flow.chain = sanitizePaths(rootDir, flow.chain);
|
|
1258
|
-
}
|
|
1259
|
-
const existing = await loadSnapshot(rootDir);
|
|
1260
|
-
if (existing) {
|
|
1261
|
-
existing.features = { ...existing.features, ...features };
|
|
1262
|
-
existing.flows = { ...existing.flows, ...flows };
|
|
1263
|
-
existing.updatedAt = now;
|
|
1264
|
-
existing.gitHash = gitHash;
|
|
1265
|
-
await saveSnapshot(rootDir, existing);
|
|
1266
|
-
return JSON.stringify({
|
|
1267
|
-
status: "updated",
|
|
1268
|
-
features: Object.keys(existing.features).length,
|
|
1269
|
-
flows: Object.keys(existing.flows).length
|
|
1270
|
-
});
|
|
1271
|
-
}
|
|
1272
|
-
const snapshot = {
|
|
1273
|
-
version: 2,
|
|
1274
|
-
createdAt: now,
|
|
1275
|
-
updatedAt: now,
|
|
1276
|
-
gitHash,
|
|
1277
|
-
features,
|
|
1278
|
-
flows
|
|
1279
|
-
};
|
|
1280
|
-
await saveSnapshot(rootDir, snapshot);
|
|
1281
|
-
return JSON.stringify({
|
|
1282
|
-
status: "created",
|
|
1283
|
-
features: Object.keys(features).length,
|
|
1284
|
-
flows: Object.keys(flows).length
|
|
1285
|
-
});
|
|
1286
|
-
}
|
|
1287
|
-
async function getImpact(dir, files) {
|
|
1288
|
-
const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
|
|
1289
|
-
const rootDir = path6.resolve(dir);
|
|
1290
|
-
const result = await analyzeImpact2(rootDir, files);
|
|
1291
|
-
return JSON.stringify(result, null, 2);
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
// src/mcp/server.ts
|
|
1295
|
-
function createMcpServer() {
|
|
1296
|
-
const server = new McpServer(
|
|
1297
|
-
{
|
|
1298
|
-
name: "mason",
|
|
1299
|
-
version: "0.3.7"
|
|
1300
|
-
},
|
|
1301
|
-
{
|
|
1302
|
-
instructions: "Mason is a context engineering tool. Always call get_snapshot before using Explore agents, Glob, or Grep to understand the codebase. The snapshot is a concept map that maps features and flows to their implementing files \u2014 it eliminates the need to search. This applies to ANY question about architecture, features, flows, how things work, cross-feature interactions, or bug investigation. Workflow: 1) Call get_snapshot first. 2) If no snapshot, call full_analysis and then save_snapshot to create one. 3) If the snapshot is stale, tell the user and offer to update it. 4) Use your native file reading tool to read files the snapshot points to. 5) Before modifying a file, call get_impact to check what else might be affected. 6) After making significant changes (new features, refactors, architecture changes), call save_snapshot to update the concept map."
|
|
1303
|
-
}
|
|
1304
|
-
);
|
|
1305
|
-
server.tool(
|
|
1306
|
-
"full_analysis",
|
|
1307
|
-
"Run a complete project analysis in one call. Returns git history stats, project structure with file counts, curated code sample previews (~60 lines each), and test-to-source file mapping. This is the recommended starting point \u2014 call this first, then read specific files natively for full content.",
|
|
1308
|
-
{
|
|
1309
|
-
dir: z.string().describe("Absolute path to the project root directory")
|
|
1310
|
-
},
|
|
1311
|
-
async ({ dir }) => {
|
|
1312
|
-
const result = await fullAnalysis(dir);
|
|
1313
|
-
return {
|
|
1314
|
-
content: [{ type: "text", text: result }]
|
|
1315
|
-
};
|
|
1316
|
-
}
|
|
1317
|
-
);
|
|
1318
|
-
server.tool(
|
|
1319
|
-
"analyze_project",
|
|
1320
|
-
"Run git history analysis on a codebase. Returns commit convention patterns, stale directories, and frequently changed files. These are aggregate stats across hundreds of commits that would be expensive to compute manually.",
|
|
1321
|
-
{
|
|
1322
|
-
dir: z.string().describe("Absolute path to the project root directory")
|
|
1323
|
-
},
|
|
1324
|
-
async ({ dir }) => {
|
|
1325
|
-
const result = await analyzeProject(dir);
|
|
1326
|
-
return {
|
|
1327
|
-
content: [{ type: "text", text: result }]
|
|
1328
|
-
};
|
|
1329
|
-
}
|
|
1330
|
-
);
|
|
1331
|
-
server.tool(
|
|
1332
|
-
"get_code_samples",
|
|
1333
|
-
"Get previews (first ~60 lines) of representative source files from the codebase. Includes entry points, config files, hot files (frequently changed), test examples, and one file per directory for breadth. Read files natively for full content.",
|
|
1334
|
-
{
|
|
1335
|
-
dir: z.string().describe("Absolute path to the project root directory"),
|
|
1336
|
-
count: z.number().optional().default(15).describe("Maximum number of files to sample (default: 15)")
|
|
1337
|
-
},
|
|
1338
|
-
async ({ dir, count }) => {
|
|
1339
|
-
const result = await getCodeSamples(dir, count);
|
|
1340
|
-
return {
|
|
1341
|
-
content: [{ type: "text", text: result }]
|
|
1342
|
-
};
|
|
1343
|
-
}
|
|
1344
|
-
);
|
|
1345
|
-
server.tool(
|
|
1346
|
-
"get_snapshot",
|
|
1347
|
-
"Get the project's concept map \u2014 a lookup table from features and flows to the files that implement them. Use this to jump straight to relevant files instead of exploring. Example: 'home screen' \u2192 [HomeScreen.kt, HomeViewModel.kt, HomeModule.kt]. If stale, run 'mason snapshot-update' to refresh.",
|
|
1348
|
-
{
|
|
1349
|
-
dir: z.string().describe("Absolute path to the project root directory")
|
|
1350
|
-
},
|
|
1351
|
-
async ({ dir }) => {
|
|
1352
|
-
const result = await getSnapshot(dir);
|
|
1353
|
-
return {
|
|
1354
|
-
content: [{ type: "text", text: result }]
|
|
1355
|
-
};
|
|
1356
|
-
}
|
|
1357
|
-
);
|
|
1358
|
-
server.tool(
|
|
1359
|
-
"save_snapshot",
|
|
1360
|
-
"Save a concept-to-files map as a persistent project snapshot. Maps feature names and data flows to the files that implement them. Persists across conversations \u2014 future sessions can call get_snapshot to instantly find relevant files. No API key needed \u2014 you are the LLM generating the map.",
|
|
1361
|
-
{
|
|
1362
|
-
dir: z.string().describe("Absolute path to the project root directory"),
|
|
1363
|
-
features: z.record(
|
|
1364
|
-
z.object({
|
|
1365
|
-
description: z.string().describe("One-line description of the feature"),
|
|
1366
|
-
files: z.array(z.string()).describe("File paths that implement this feature"),
|
|
1367
|
-
tests: z.array(z.string()).optional().describe("Test file paths for this feature")
|
|
1368
|
-
})
|
|
1369
|
-
).describe("Map of feature names to their implementing files"),
|
|
1370
|
-
flows: z.record(
|
|
1371
|
-
z.object({
|
|
1372
|
-
description: z.string().describe("One-line description of the flow"),
|
|
1373
|
-
chain: z.array(z.string()).describe("Ordered list of file paths showing data/call flow")
|
|
1374
|
-
})
|
|
1375
|
-
).describe("Map of flow names to ordered file chains")
|
|
1376
|
-
},
|
|
1377
|
-
async ({ dir, features, flows }) => {
|
|
1378
|
-
const result = await saveSnapshotData(dir, features, flows);
|
|
1379
|
-
return {
|
|
1380
|
-
content: [{ type: "text", text: result }]
|
|
1381
|
-
};
|
|
1382
|
-
}
|
|
1383
|
-
);
|
|
1384
|
-
server.tool(
|
|
1385
|
-
"get_impact",
|
|
1386
|
-
"Analyze the impact of changing specific files. Returns three signals: git co-change (files that historically change together), references (files that mention the target by name), and related tests. Use this before editing a file to understand what else might need updating.",
|
|
1387
|
-
{
|
|
1388
|
-
dir: z.string().describe("Absolute path to the project root directory"),
|
|
1389
|
-
files: z.array(z.string()).describe("File paths or names to analyze (e.g., ['WeatherRepository.kt'] or ['src/services/auth.ts'])")
|
|
1390
|
-
},
|
|
1391
|
-
async ({ dir, files }) => {
|
|
1392
|
-
const result = await getImpact(dir, files);
|
|
1393
|
-
return {
|
|
1394
|
-
content: [{ type: "text", text: result }]
|
|
1395
|
-
};
|
|
1396
|
-
}
|
|
1397
|
-
);
|
|
1398
|
-
return server;
|
|
1399
|
-
}
|
|
1400
|
-
async function startMcpServer() {
|
|
1401
|
-
const server = createMcpServer();
|
|
1402
|
-
const transport = new StdioServerTransport();
|
|
1403
|
-
await server.connect(transport);
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
// bin/mason-mcp.ts
|
|
1407
|
-
startMcpServer().catch((err) => {
|
|
1408
|
-
process.stderr.write(`Mason MCP server error: ${err}
|
|
1409
|
-
`);
|
|
1410
|
-
process.exit(1);
|
|
1411
|
-
});
|
|
1412
|
-
//# sourceMappingURL=mason-mcp.js.map
|