mason-context 0.2.3 → 0.3.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.
@@ -9,18 +9,115 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
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
+
12
109
  // src/impact/impact.ts
13
110
  var impact_exports = {};
14
111
  __export(impact_exports, {
15
112
  analyzeImpact: () => analyzeImpact
16
113
  });
17
114
  import fs5 from "fs/promises";
18
- import path4 from "path";
115
+ import path5 from "path";
19
116
  import { execFile as execFile7 } from "child_process";
20
117
  import { promisify as promisify7 } from "util";
21
- import fg3 from "fast-glob";
118
+ import fg4 from "fast-glob";
22
119
  async function analyzeImpact(rootDir, targetFiles) {
23
- const resolvedRoot = path4.resolve(rootDir);
120
+ const resolvedRoot = path5.resolve(rootDir);
24
121
  const resolvedTargets = await resolveTargetFiles(resolvedRoot, targetFiles);
25
122
  const [cochange, references, tests] = await Promise.all([
26
123
  getCochangeFiles(resolvedRoot, resolvedTargets),
@@ -41,17 +138,17 @@ async function resolveTargetFiles(rootDir, targets) {
41
138
  resolved.push(target);
42
139
  continue;
43
140
  }
44
- const matches = await fg3(`**/${target}`, {
141
+ const matches = await fg4(`**/${target}`, {
45
142
  cwd: rootDir,
46
- ignore: IGNORE
143
+ ignore: IGNORE2
47
144
  });
48
145
  if (matches.length > 0) {
49
146
  resolved.push(matches[0]);
50
147
  } else {
51
148
  const noExt = target.replace(/\.[^.]+$/, "");
52
- const extMatches = await fg3(`**/${noExt}.*`, {
149
+ const extMatches = await fg4(`**/${noExt}.*`, {
53
150
  cwd: rootDir,
54
- ignore: IGNORE
151
+ ignore: IGNORE2
55
152
  });
56
153
  if (extMatches.length > 0) {
57
154
  resolved.push(extMatches[0]);
@@ -103,12 +200,12 @@ async function getCochangeFiles(rootDir, targetFiles) {
103
200
  async function getReferences(rootDir, targetFiles) {
104
201
  const searchNames = /* @__PURE__ */ new Set();
105
202
  for (const target of targetFiles) {
106
- const basename = path4.basename(target).replace(/\.[^.]+$/, "");
203
+ const basename = path5.basename(target).replace(/\.[^.]+$/, "");
107
204
  searchNames.add(basename);
108
205
  }
109
- const allSourceFiles = await fg3(`**/${SOURCE_EXTENSIONS2}`, {
206
+ const allSourceFiles = await fg4(`**/${SOURCE_EXTENSIONS2}`, {
110
207
  cwd: rootDir,
111
- ignore: IGNORE
208
+ ignore: IGNORE2
112
209
  });
113
210
  const targetSet = new Set(targetFiles);
114
211
  const filesToSearch = allSourceFiles.filter((f) => !targetSet.has(f));
@@ -120,7 +217,7 @@ async function getReferences(rootDir, targetFiles) {
120
217
  batch.map(async (file) => {
121
218
  try {
122
219
  const content = await fs5.readFile(
123
- path4.join(rootDir, file),
220
+ path5.join(rootDir, file),
124
221
  "utf-8"
125
222
  );
126
223
  for (const name of searchNames) {
@@ -155,12 +252,12 @@ async function getRelatedTests(rootDir, targetFiles) {
155
252
  "**/*Test.swift",
156
253
  "**/*_test.rs"
157
254
  ];
158
- const testFiles = await fg3(testPatterns, { cwd: rootDir, ignore: IGNORE });
255
+ const testFiles = await fg4(testPatterns, { cwd: rootDir, ignore: IGNORE2 });
159
256
  const results = [];
160
257
  for (const target of targetFiles) {
161
- const targetBaseName = path4.basename(target).replace(/\.[^.]+$/, "");
258
+ const targetBaseName = path5.basename(target).replace(/\.[^.]+$/, "");
162
259
  for (const testFile of testFiles) {
163
- const testBaseName = path4.basename(testFile).replace(/\.[^.]+$/, "");
260
+ const testBaseName = path5.basename(testFile).replace(/\.[^.]+$/, "");
164
261
  const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
165
262
  if (sourceName === targetBaseName) {
166
263
  results.push({
@@ -175,12 +272,12 @@ async function getRelatedTests(rootDir, targetFiles) {
175
272
  function escapeRegex(str) {
176
273
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
177
274
  }
178
- var exec7, IGNORE, SOURCE_EXTENSIONS2;
275
+ var exec7, IGNORE2, SOURCE_EXTENSIONS2;
179
276
  var init_impact = __esm({
180
277
  "src/impact/impact.ts"() {
181
278
  "use strict";
182
279
  exec7 = promisify7(execFile7);
183
- IGNORE = [
280
+ IGNORE2 = [
184
281
  "**/node_modules/**",
185
282
  "**/dist/**",
186
283
  "**/build/**",
@@ -204,10 +301,10 @@ import { z } from "zod";
204
301
 
205
302
  // src/mcp/tools.ts
206
303
  import fs6 from "fs/promises";
207
- import path5 from "path";
304
+ import path6 from "path";
208
305
  import { execFile as execFile8 } from "child_process";
209
306
  import { promisify as promisify8 } from "util";
210
- import fg4 from "fast-glob";
307
+ import fg5 from "fast-glob";
211
308
 
212
309
  // src/analyzers/git-history.ts
213
310
  import { execFile } from "child_process";
@@ -810,23 +907,24 @@ function isSensitiveFile(filePath) {
810
907
 
811
908
  // src/snapshot/snapshot.ts
812
909
  import fs4 from "fs/promises";
813
- import path3 from "path";
910
+ import path4 from "path";
814
911
  import { execFile as execFile6 } from "child_process";
815
912
  import { promisify as promisify6 } from "util";
913
+ init_test_map();
816
914
 
817
915
  // src/llm/providers.ts
818
- import { execFile as execFile5 } from "child_process";
916
+ import { execFile as execFile5, spawn } from "child_process";
819
917
  import { promisify as promisify5 } from "util";
820
918
 
821
919
  // src/llm/config.ts
822
920
  import fs3 from "fs/promises";
823
- import path2 from "path";
921
+ import path3 from "path";
824
922
  import os from "os";
825
923
  import { execFile as execFile4 } from "child_process";
826
924
  import { promisify as promisify4 } from "util";
827
925
  var exec4 = promisify4(execFile4);
828
- var CONFIG_DIR = path2.join(os.homedir(), ".mason");
829
- var CONFIG_FILE = path2.join(CONFIG_DIR, "config.json");
926
+ var CONFIG_DIR = path3.join(os.homedir(), ".mason");
927
+ var CONFIG_FILE = path3.join(CONFIG_DIR, "config.json");
830
928
 
831
929
  // src/llm/providers.ts
832
930
  var exec5 = promisify5(execFile5);
@@ -834,10 +932,10 @@ var exec5 = promisify5(execFile5);
834
932
  // src/snapshot/snapshot.ts
835
933
  var exec6 = promisify6(execFile6);
836
934
  function snapshotDir(rootDir) {
837
- return path3.join(rootDir, ".mason");
935
+ return path4.join(rootDir, ".mason");
838
936
  }
839
937
  function snapshotPath(rootDir) {
840
- return path3.join(snapshotDir(rootDir), "snapshot.json");
938
+ return path4.join(snapshotDir(rootDir), "snapshot.json");
841
939
  }
842
940
  async function loadSnapshot(rootDir) {
843
941
  try {
@@ -870,7 +968,7 @@ async function getCurrentGitHash(rootDir) {
870
968
 
871
969
  // src/mcp/tools.ts
872
970
  var exec8 = promisify8(execFile8);
873
- var IGNORE2 = [
971
+ var IGNORE3 = [
874
972
  "**/node_modules/**",
875
973
  "**/dist/**",
876
974
  "**/build/**",
@@ -891,7 +989,7 @@ async function buildContext(dir) {
891
989
  };
892
990
  }
893
991
  async function analyzeProject(dir) {
894
- const rootDir = path5.resolve(dir);
992
+ const rootDir = path6.resolve(dir);
895
993
  const context = await buildContext(rootDir);
896
994
  const results = await runAll(context);
897
995
  const projectSnapshot = await detectProjectSnapshot(rootDir);
@@ -945,7 +1043,7 @@ async function detectProjectSnapshot(rootDir) {
945
1043
  const present = [];
946
1044
  for (const file of buildFiles) {
947
1045
  try {
948
- await fs6.access(path5.join(rootDir, file));
1046
+ await fs6.access(path6.join(rootDir, file));
949
1047
  present.push(file);
950
1048
  } catch {
951
1049
  }
@@ -963,9 +1061,9 @@ async function detectProjectSnapshot(rootDir) {
963
1061
  ];
964
1062
  const testInfo = {};
965
1063
  for (const pattern of testDirs) {
966
- const files = await fg4(`${pattern}/**/*`, {
1064
+ const files = await fg5(`${pattern}/**/*`, {
967
1065
  cwd: rootDir,
968
- ignore: IGNORE2,
1066
+ ignore: IGNORE3,
969
1067
  onlyFiles: true
970
1068
  });
971
1069
  if (files.length > 0) {
@@ -983,18 +1081,18 @@ async function detectProjectSnapshot(rootDir) {
983
1081
  { pattern: "**/*_test.rs", label: "*_test.rs" }
984
1082
  ];
985
1083
  for (const { pattern, label } of testFilePatterns) {
986
- const files = await fg4(pattern, { cwd: rootDir, ignore: IGNORE2 });
1084
+ const files = await fg5(pattern, { cwd: rootDir, ignore: IGNORE3 });
987
1085
  if (files.length > 0) {
988
1086
  testInfo[label] = files.length;
989
1087
  }
990
1088
  }
991
- const sourceFiles = await fg4("**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}", {
1089
+ const sourceFiles = await fg5("**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}", {
992
1090
  cwd: rootDir,
993
- ignore: IGNORE2
1091
+ ignore: IGNORE3
994
1092
  });
995
1093
  const fileCounts = {};
996
1094
  for (const file of sourceFiles) {
997
- const ext = path5.extname(file).slice(1);
1095
+ const ext = path6.extname(file).slice(1);
998
1096
  fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
999
1097
  }
1000
1098
  return {
@@ -1005,7 +1103,7 @@ async function detectProjectSnapshot(rootDir) {
1005
1103
  };
1006
1104
  }
1007
1105
  async function getCodeSamples(dir, count = 15) {
1008
- const rootDir = path5.resolve(dir);
1106
+ const rootDir = path6.resolve(dir);
1009
1107
  const samples = await sampleFiles(rootDir, count);
1010
1108
  const output = {
1011
1109
  note: "These are previews (first ~60 lines). Use get_file_content to read the full file if needed.",
@@ -1020,10 +1118,10 @@ async function getCodeSamples(dir, count = 15) {
1020
1118
  return JSON.stringify(output, null, 2);
1021
1119
  }
1022
1120
  async function getProjectStructure(dir) {
1023
- const rootDir = path5.resolve(dir);
1024
- const allFiles = await fg4("**/*", {
1121
+ const rootDir = path6.resolve(dir);
1122
+ const allFiles = await fg5("**/*", {
1025
1123
  cwd: rootDir,
1026
- ignore: IGNORE2,
1124
+ ignore: IGNORE3,
1027
1125
  onlyFiles: true
1028
1126
  });
1029
1127
  const dirInfo = /* @__PURE__ */ new Map();
@@ -1036,7 +1134,7 @@ async function getProjectStructure(dir) {
1036
1134
  }
1037
1135
  const info = dirInfo.get(dirPath);
1038
1136
  info.fileCount++;
1039
- const ext = path5.extname(file).slice(1);
1137
+ const ext = path6.extname(file).slice(1);
1040
1138
  if (ext) {
1041
1139
  info.extensions.set(ext, (info.extensions.get(ext) ?? 0) + 1);
1042
1140
  }
@@ -1058,81 +1156,12 @@ async function getProjectStructure(dir) {
1058
1156
  return JSON.stringify(output, null, 2);
1059
1157
  }
1060
1158
  async function getTestMap(dir) {
1061
- const rootDir = path5.resolve(dir);
1062
- const testPatterns = [
1063
- "**/*.test.*",
1064
- "**/*.spec.*",
1065
- "**/*Test.kt",
1066
- "**/*Test.java",
1067
- "**/*Tests.kt",
1068
- "**/*Tests.java",
1069
- "**/test_*.py",
1070
- "**/*_test.py",
1071
- "**/*_test.go",
1072
- "**/*Tests.swift",
1073
- "**/*Test.swift",
1074
- "**/*_test.rs"
1075
- ];
1076
- const testFiles = await fg4(testPatterns, { cwd: rootDir, ignore: IGNORE2 });
1077
- const sourceFiles = await fg4(
1078
- "**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,dart}",
1079
- { cwd: rootDir, ignore: IGNORE2 }
1080
- );
1081
- const sourceByBaseName = /* @__PURE__ */ new Map();
1082
- for (const file of sourceFiles) {
1083
- if (testFiles.includes(file)) continue;
1084
- const baseName = path5.basename(file).replace(/\.[^.]+$/, "");
1085
- const existing = sourceByBaseName.get(baseName) ?? [];
1086
- existing.push(file);
1087
- sourceByBaseName.set(baseName, existing);
1088
- }
1089
- const pairs = [];
1090
- const unmatched = [];
1091
- for (const testFile of testFiles) {
1092
- const testBaseName = path5.basename(testFile).replace(/\.[^.]+$/, "");
1093
- const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
1094
- if (!sourceName) {
1095
- unmatched.push(testFile);
1096
- continue;
1097
- }
1098
- const candidates = sourceByBaseName.get(sourceName);
1099
- if (candidates && candidates.length > 0) {
1100
- const testDir = path5.dirname(testFile);
1101
- const bestMatch = candidates.reduce((best, candidate) => {
1102
- const candidateDir = path5.dirname(candidate);
1103
- const bestDir = path5.dirname(best);
1104
- const candidateOverlap = commonSegments(testDir, candidateDir);
1105
- const bestOverlap = commonSegments(testDir, bestDir);
1106
- return candidateOverlap > bestOverlap ? candidate : best;
1107
- });
1108
- pairs.push({
1109
- test: testFile,
1110
- source: bestMatch,
1111
- confidence: candidates.length === 1 ? "exact" : "best-guess"
1112
- });
1113
- } else {
1114
- unmatched.push(testFile);
1115
- }
1116
- }
1117
- const output = {
1118
- totalTestFiles: testFiles.length,
1119
- paired: pairs,
1120
- unmatched
1121
- };
1122
- return JSON.stringify(output, null, 2);
1123
- }
1124
- function commonSegments(pathA, pathB) {
1125
- const segsA = pathA.split("/");
1126
- const segsB = pathB.split("/");
1127
- let count = 0;
1128
- for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
1129
- if (segsA[i] === segsB[i]) count++;
1130
- else break;
1131
- }
1132
- return count;
1159
+ const { buildTestMap: buildTestMap2 } = await Promise.resolve().then(() => (init_test_map(), test_map_exports));
1160
+ const result = await buildTestMap2(dir);
1161
+ return JSON.stringify(result, null, 2);
1133
1162
  }
1134
1163
  async function getSnapshot(dir) {
1135
- const rootDir = path5.resolve(dir);
1164
+ const rootDir = path6.resolve(dir);
1136
1165
  const snapshot = await loadSnapshot(rootDir);
1137
1166
  if (!snapshot) {
1138
1167
  return JSON.stringify({
@@ -1148,7 +1177,11 @@ async function getSnapshot(dir) {
1148
1177
  const unique = feat.files.filter((f) => !seenFiles.has(f));
1149
1178
  if (unique.length === 0) continue;
1150
1179
  for (const f of unique) seenFiles.add(f);
1151
- compactFeatures[name] = unique;
1180
+ const entry = { files: unique };
1181
+ if (feat.tests && feat.tests.length > 0) {
1182
+ entry.tests = feat.tests;
1183
+ }
1184
+ compactFeatures[name] = entry;
1152
1185
  }
1153
1186
  const compactFlows = {};
1154
1187
  for (const [name, flow] of Object.entries(snapshot.flows)) {
@@ -1156,6 +1189,7 @@ async function getSnapshot(dir) {
1156
1189
  }
1157
1190
  const output = {
1158
1191
  exists: true,
1192
+ updatedAt: snapshot.updatedAt,
1159
1193
  features: compactFeatures,
1160
1194
  flows: compactFlows,
1161
1195
  stale: isStale
@@ -1166,7 +1200,7 @@ async function getSnapshot(dir) {
1166
1200
  return JSON.stringify(output);
1167
1201
  }
1168
1202
  async function fullAnalysis(dir) {
1169
- const rootDir = path5.resolve(dir);
1203
+ const rootDir = path6.resolve(dir);
1170
1204
  const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
1171
1205
  analyzeProject(dir),
1172
1206
  getProjectStructure(dir),
@@ -1192,7 +1226,7 @@ async function fullAnalysis(dir) {
1192
1226
  return JSON.stringify(output, null, 2);
1193
1227
  }
1194
1228
  async function saveSnapshotData(dir, features, flows) {
1195
- const rootDir = path5.resolve(dir);
1229
+ const rootDir = path6.resolve(dir);
1196
1230
  const gitHash = await getCurrentGitHash(rootDir);
1197
1231
  const now = (/* @__PURE__ */ new Date()).toISOString();
1198
1232
  const existing = await loadSnapshot(rootDir);
@@ -1225,7 +1259,7 @@ async function saveSnapshotData(dir, features, flows) {
1225
1259
  }
1226
1260
  async function getImpact(dir, files) {
1227
1261
  const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
1228
- const rootDir = path5.resolve(dir);
1262
+ const rootDir = path6.resolve(dir);
1229
1263
  const result = await analyzeImpact2(rootDir, files);
1230
1264
  return JSON.stringify(result, null, 2);
1231
1265
  }
@@ -1235,7 +1269,7 @@ function createMcpServer() {
1235
1269
  const server = new McpServer(
1236
1270
  {
1237
1271
  name: "mason",
1238
- version: "0.2.3"
1272
+ version: "0.3.0"
1239
1273
  },
1240
1274
  {
1241
1275
  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."