mason-context 0.2.2 → 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.
package/dist/bin/mason.js CHANGED
@@ -1,12 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
- }) : x)(function(x) {
7
- if (typeof require !== "undefined") return require.apply(this, arguments);
8
- throw Error('Dynamic require of "' + x + '" is not supported');
9
- });
10
4
  var __esm = (fn, res) => function __init() {
11
5
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
6
  };
@@ -320,7 +314,7 @@ var init_config = __esm({
320
314
  });
321
315
 
322
316
  // src/llm/providers.ts
323
- import { execFile as execFile4 } from "child_process";
317
+ import { execFile as execFile4, spawn } from "child_process";
324
318
  import { promisify as promisify4 } from "util";
325
319
  async function callLLM(config, userMessage, systemPrompt) {
326
320
  const model = config.model ?? getDefaultModel(config.provider);
@@ -379,7 +373,6 @@ function formatPromptForCopy(system, userMessage) {
379
373
  ${userMessage}`;
380
374
  }
381
375
  function spawnWithStdin(command, args, input) {
382
- const { spawn } = __require("child_process");
383
376
  return new Promise((resolve, reject) => {
384
377
  const proc = spawn(command, args, {
385
378
  stdio: ["pipe", "pipe", "pipe"],
@@ -902,15 +895,121 @@ var init_sampler = __esm({
902
895
  }
903
896
  });
904
897
 
898
+ // src/test-map.ts
899
+ var test_map_exports = {};
900
+ __export(test_map_exports, {
901
+ buildTestMap: () => buildTestMap
902
+ });
903
+ import path3 from "path";
904
+ import fg3 from "fast-glob";
905
+ async function buildTestMap(dir) {
906
+ const rootDir = path3.resolve(dir);
907
+ const testPatterns = [
908
+ "**/*.test.*",
909
+ "**/*.spec.*",
910
+ "**/*Test.kt",
911
+ "**/*Test.java",
912
+ "**/*Tests.kt",
913
+ "**/*Tests.java",
914
+ "**/test_*.py",
915
+ "**/*_test.py",
916
+ "**/*_test.go",
917
+ "**/*Tests.swift",
918
+ "**/*Test.swift",
919
+ "**/*_test.rs"
920
+ ];
921
+ const testFiles = await fg3(testPatterns, { cwd: rootDir, ignore: IGNORE });
922
+ const sourceFiles = await fg3(
923
+ "**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,dart}",
924
+ { cwd: rootDir, ignore: IGNORE }
925
+ );
926
+ const sourceByBaseName = /* @__PURE__ */ new Map();
927
+ for (const file of sourceFiles) {
928
+ if (testFiles.includes(file)) continue;
929
+ const baseName = path3.basename(file).replace(/\.[^.]+$/, "");
930
+ const existing = sourceByBaseName.get(baseName) ?? [];
931
+ existing.push(file);
932
+ sourceByBaseName.set(baseName, existing);
933
+ }
934
+ const paired = [];
935
+ const unmatched = [];
936
+ for (const testFile of testFiles) {
937
+ const testBaseName = path3.basename(testFile).replace(/\.[^.]+$/, "");
938
+ const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
939
+ if (!sourceName) {
940
+ unmatched.push(testFile);
941
+ continue;
942
+ }
943
+ const candidates = sourceByBaseName.get(sourceName);
944
+ if (candidates && candidates.length > 0) {
945
+ const testDir = path3.dirname(testFile);
946
+ const bestMatch = candidates.reduce((best, candidate) => {
947
+ const candidateDir = path3.dirname(candidate);
948
+ const bestDir = path3.dirname(best);
949
+ const candidateOverlap = commonSegments(testDir, candidateDir);
950
+ const bestOverlap = commonSegments(testDir, bestDir);
951
+ return candidateOverlap > bestOverlap ? candidate : best;
952
+ });
953
+ paired.push({
954
+ test: testFile,
955
+ source: bestMatch,
956
+ confidence: candidates.length === 1 ? "exact" : "best-guess"
957
+ });
958
+ } else {
959
+ unmatched.push(testFile);
960
+ }
961
+ }
962
+ return { totalTestFiles: testFiles.length, paired, unmatched };
963
+ }
964
+ function commonSegments(pathA, pathB) {
965
+ const segsA = pathA.split("/");
966
+ const segsB = pathB.split("/");
967
+ let count = 0;
968
+ for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
969
+ if (segsA[i] === segsB[i]) count++;
970
+ else break;
971
+ }
972
+ return count;
973
+ }
974
+ var IGNORE;
975
+ var init_test_map = __esm({
976
+ "src/test-map.ts"() {
977
+ "use strict";
978
+ IGNORE = [
979
+ "**/node_modules/**",
980
+ "**/dist/**",
981
+ "**/build/**",
982
+ "**/.gradle/**",
983
+ "**/target/**",
984
+ "**/.git/**",
985
+ "**/vendor/**",
986
+ "**/__pycache__/**",
987
+ "**/venv/**",
988
+ "**/.venv/**",
989
+ "**/*.min.*",
990
+ "**/*.map"
991
+ ];
992
+ }
993
+ });
994
+
905
995
  // src/snapshot/prompt.ts
906
- function buildSnapshotPrompt(files) {
996
+ function buildSnapshotPrompt(files, testPairs) {
907
997
  const fileBlocks = files.map(
908
998
  (f) => `=== ${f.path} ===
909
999
  ${f.content.slice(0, 3e3)}${f.content.length > 3e3 ? "\n... (truncated)" : ""}`
910
1000
  ).join("\n\n");
911
- return `Create a concept-to-files map for this codebase. Here are the key source files:
1001
+ let prompt = `Create a concept-to-files map for this codebase. Here are the key source files:
912
1002
 
913
1003
  ${fileBlocks}`;
1004
+ if (testPairs && testPairs.length > 0) {
1005
+ const testBlock = testPairs.map((p) => `${p.test} \u2192 ${p.source}`).join("\n");
1006
+ prompt += `
1007
+
1008
+ Here are the test-to-source file mappings. Use these to populate the "tests" field for each feature:
1009
+
1010
+ ${testBlock}`;
1011
+ }
1012
+ return prompt;
914
1013
  }
915
1014
  function buildIncrementalPrompt(files, existingSnapshot) {
916
1015
  const fileBlocks = files.map(
@@ -986,14 +1085,14 @@ __export(snapshot_exports, {
986
1085
  updateSnapshot: () => updateSnapshot
987
1086
  });
988
1087
  import fs4 from "fs/promises";
989
- import path3 from "path";
1088
+ import path4 from "path";
990
1089
  import { execFile as execFile6 } from "child_process";
991
1090
  import { promisify as promisify6 } from "util";
992
1091
  function snapshotDir(rootDir) {
993
- return path3.join(rootDir, ".mason");
1092
+ return path4.join(rootDir, ".mason");
994
1093
  }
995
1094
  function snapshotPath(rootDir) {
996
- return path3.join(snapshotDir(rootDir), "snapshot.json");
1095
+ return path4.join(snapshotDir(rootDir), "snapshot.json");
997
1096
  }
998
1097
  async function loadSnapshot(rootDir) {
999
1098
  try {
@@ -1051,7 +1150,7 @@ function parseSnapshotResponse(raw) {
1051
1150
  }
1052
1151
  }
1053
1152
  async function createSnapshot(rootDir, config) {
1054
- const resolvedRoot = path3.resolve(rootDir);
1153
+ const resolvedRoot = path4.resolve(rootDir);
1055
1154
  const sampled = await sampleFiles(resolvedRoot, 25);
1056
1155
  const filesWithContent = [];
1057
1156
  for (const sample of sampled) {
@@ -1072,7 +1171,8 @@ async function createSnapshot(rootDir, config) {
1072
1171
  flows: {}
1073
1172
  };
1074
1173
  }
1075
- const userMessage = buildSnapshotPrompt(filesWithContent);
1174
+ const testMap = await buildTestMap(resolvedRoot);
1175
+ const userMessage = buildSnapshotPrompt(filesWithContent, testMap.paired);
1076
1176
  const result = await callLLM(config, userMessage, SNAPSHOT_SYSTEM_PROMPT);
1077
1177
  const resultText = typeof result === "string" ? result : result.type === "response" ? result.text : "";
1078
1178
  if (!resultText) {
@@ -1093,7 +1193,7 @@ async function createSnapshot(rootDir, config) {
1093
1193
  return snapshot;
1094
1194
  }
1095
1195
  async function updateSnapshot(rootDir, config) {
1096
- const resolvedRoot = path3.resolve(rootDir);
1196
+ const resolvedRoot = path4.resolve(rootDir);
1097
1197
  const existing = await loadSnapshot(resolvedRoot);
1098
1198
  if (!existing) {
1099
1199
  const snapshot = await createSnapshot(rootDir, config);
@@ -1174,14 +1274,14 @@ async function updateSnapshot(rootDir, config) {
1174
1274
  };
1175
1275
  }
1176
1276
  async function installHook(rootDir) {
1177
- const resolvedRoot = path3.resolve(rootDir);
1178
- const hooksDir = path3.join(resolvedRoot, ".git", "hooks");
1277
+ const resolvedRoot = path4.resolve(rootDir);
1278
+ const hooksDir = path4.join(resolvedRoot, ".git", "hooks");
1179
1279
  try {
1180
1280
  await fs4.access(hooksDir);
1181
1281
  } catch {
1182
1282
  throw new Error("Not a git repository (no .git/hooks directory)");
1183
1283
  }
1184
- const hookPath = path3.join(hooksDir, "post-commit");
1284
+ const hookPath = path4.join(hooksDir, "post-commit");
1185
1285
  const hookContent = `#!/bin/sh
1186
1286
  # Mason: auto-update project snapshot after commit
1187
1287
  # Runs in background so it doesn't block your workflow
@@ -1202,6 +1302,7 @@ var init_snapshot = __esm({
1202
1302
  "src/snapshot/snapshot.ts"() {
1203
1303
  "use strict";
1204
1304
  init_sampler();
1305
+ init_test_map();
1205
1306
  init_providers();
1206
1307
  init_prompt();
1207
1308
  exec6 = promisify6(execFile6);
@@ -1214,12 +1315,12 @@ __export(impact_exports, {
1214
1315
  analyzeImpact: () => analyzeImpact
1215
1316
  });
1216
1317
  import fs5 from "fs/promises";
1217
- import path4 from "path";
1318
+ import path5 from "path";
1218
1319
  import { execFile as execFile7 } from "child_process";
1219
1320
  import { promisify as promisify7 } from "util";
1220
- import fg3 from "fast-glob";
1321
+ import fg4 from "fast-glob";
1221
1322
  async function analyzeImpact(rootDir, targetFiles) {
1222
- const resolvedRoot = path4.resolve(rootDir);
1323
+ const resolvedRoot = path5.resolve(rootDir);
1223
1324
  const resolvedTargets = await resolveTargetFiles(resolvedRoot, targetFiles);
1224
1325
  const [cochange, references, tests] = await Promise.all([
1225
1326
  getCochangeFiles(resolvedRoot, resolvedTargets),
@@ -1240,17 +1341,17 @@ async function resolveTargetFiles(rootDir, targets) {
1240
1341
  resolved.push(target);
1241
1342
  continue;
1242
1343
  }
1243
- const matches = await fg3(`**/${target}`, {
1344
+ const matches = await fg4(`**/${target}`, {
1244
1345
  cwd: rootDir,
1245
- ignore: IGNORE
1346
+ ignore: IGNORE2
1246
1347
  });
1247
1348
  if (matches.length > 0) {
1248
1349
  resolved.push(matches[0]);
1249
1350
  } else {
1250
1351
  const noExt = target.replace(/\.[^.]+$/, "");
1251
- const extMatches = await fg3(`**/${noExt}.*`, {
1352
+ const extMatches = await fg4(`**/${noExt}.*`, {
1252
1353
  cwd: rootDir,
1253
- ignore: IGNORE
1354
+ ignore: IGNORE2
1254
1355
  });
1255
1356
  if (extMatches.length > 0) {
1256
1357
  resolved.push(extMatches[0]);
@@ -1302,12 +1403,12 @@ async function getCochangeFiles(rootDir, targetFiles) {
1302
1403
  async function getReferences(rootDir, targetFiles) {
1303
1404
  const searchNames = /* @__PURE__ */ new Set();
1304
1405
  for (const target of targetFiles) {
1305
- const basename = path4.basename(target).replace(/\.[^.]+$/, "");
1406
+ const basename = path5.basename(target).replace(/\.[^.]+$/, "");
1306
1407
  searchNames.add(basename);
1307
1408
  }
1308
- const allSourceFiles = await fg3(`**/${SOURCE_EXTENSIONS2}`, {
1409
+ const allSourceFiles = await fg4(`**/${SOURCE_EXTENSIONS2}`, {
1309
1410
  cwd: rootDir,
1310
- ignore: IGNORE
1411
+ ignore: IGNORE2
1311
1412
  });
1312
1413
  const targetSet = new Set(targetFiles);
1313
1414
  const filesToSearch = allSourceFiles.filter((f) => !targetSet.has(f));
@@ -1319,7 +1420,7 @@ async function getReferences(rootDir, targetFiles) {
1319
1420
  batch.map(async (file) => {
1320
1421
  try {
1321
1422
  const content = await fs5.readFile(
1322
- path4.join(rootDir, file),
1423
+ path5.join(rootDir, file),
1323
1424
  "utf-8"
1324
1425
  );
1325
1426
  for (const name of searchNames) {
@@ -1354,12 +1455,12 @@ async function getRelatedTests(rootDir, targetFiles) {
1354
1455
  "**/*Test.swift",
1355
1456
  "**/*_test.rs"
1356
1457
  ];
1357
- const testFiles = await fg3(testPatterns, { cwd: rootDir, ignore: IGNORE });
1458
+ const testFiles = await fg4(testPatterns, { cwd: rootDir, ignore: IGNORE2 });
1358
1459
  const results = [];
1359
1460
  for (const target of targetFiles) {
1360
- const targetBaseName = path4.basename(target).replace(/\.[^.]+$/, "");
1461
+ const targetBaseName = path5.basename(target).replace(/\.[^.]+$/, "");
1361
1462
  for (const testFile of testFiles) {
1362
- const testBaseName = path4.basename(testFile).replace(/\.[^.]+$/, "");
1463
+ const testBaseName = path5.basename(testFile).replace(/\.[^.]+$/, "");
1363
1464
  const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
1364
1465
  if (sourceName === targetBaseName) {
1365
1466
  results.push({
@@ -1374,12 +1475,12 @@ async function getRelatedTests(rootDir, targetFiles) {
1374
1475
  function escapeRegex(str) {
1375
1476
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1376
1477
  }
1377
- var exec7, IGNORE, SOURCE_EXTENSIONS2;
1478
+ var exec7, IGNORE2, SOURCE_EXTENSIONS2;
1378
1479
  var init_impact = __esm({
1379
1480
  "src/impact/impact.ts"() {
1380
1481
  "use strict";
1381
1482
  exec7 = promisify7(execFile7);
1382
- IGNORE = [
1483
+ IGNORE2 = [
1383
1484
  "**/node_modules/**",
1384
1485
  "**/dist/**",
1385
1486
  "**/build/**",
@@ -1398,10 +1499,10 @@ var init_impact = __esm({
1398
1499
 
1399
1500
  // src/mcp/tools.ts
1400
1501
  import fs6 from "fs/promises";
1401
- import path5 from "path";
1502
+ import path6 from "path";
1402
1503
  import { execFile as execFile8 } from "child_process";
1403
1504
  import { promisify as promisify8 } from "util";
1404
- import fg4 from "fast-glob";
1505
+ import fg5 from "fast-glob";
1405
1506
  async function buildContext(dir) {
1406
1507
  return {
1407
1508
  rootDir: dir,
@@ -1409,7 +1510,7 @@ async function buildContext(dir) {
1409
1510
  };
1410
1511
  }
1411
1512
  async function analyzeProject(dir) {
1412
- const rootDir = path5.resolve(dir);
1513
+ const rootDir = path6.resolve(dir);
1413
1514
  const context = await buildContext(rootDir);
1414
1515
  const results = await runAll(context);
1415
1516
  const projectSnapshot = await detectProjectSnapshot(rootDir);
@@ -1463,7 +1564,7 @@ async function detectProjectSnapshot(rootDir) {
1463
1564
  const present = [];
1464
1565
  for (const file of buildFiles) {
1465
1566
  try {
1466
- await fs6.access(path5.join(rootDir, file));
1567
+ await fs6.access(path6.join(rootDir, file));
1467
1568
  present.push(file);
1468
1569
  } catch {
1469
1570
  }
@@ -1481,9 +1582,9 @@ async function detectProjectSnapshot(rootDir) {
1481
1582
  ];
1482
1583
  const testInfo = {};
1483
1584
  for (const pattern of testDirs) {
1484
- const files = await fg4(`${pattern}/**/*`, {
1585
+ const files = await fg5(`${pattern}/**/*`, {
1485
1586
  cwd: rootDir,
1486
- ignore: IGNORE2,
1587
+ ignore: IGNORE3,
1487
1588
  onlyFiles: true
1488
1589
  });
1489
1590
  if (files.length > 0) {
@@ -1501,18 +1602,18 @@ async function detectProjectSnapshot(rootDir) {
1501
1602
  { pattern: "**/*_test.rs", label: "*_test.rs" }
1502
1603
  ];
1503
1604
  for (const { pattern, label } of testFilePatterns) {
1504
- const files = await fg4(pattern, { cwd: rootDir, ignore: IGNORE2 });
1605
+ const files = await fg5(pattern, { cwd: rootDir, ignore: IGNORE3 });
1505
1606
  if (files.length > 0) {
1506
1607
  testInfo[label] = files.length;
1507
1608
  }
1508
1609
  }
1509
- const sourceFiles = await fg4("**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}", {
1610
+ const sourceFiles = await fg5("**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}", {
1510
1611
  cwd: rootDir,
1511
- ignore: IGNORE2
1612
+ ignore: IGNORE3
1512
1613
  });
1513
1614
  const fileCounts = {};
1514
1615
  for (const file of sourceFiles) {
1515
- const ext = path5.extname(file).slice(1);
1616
+ const ext = path6.extname(file).slice(1);
1516
1617
  fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
1517
1618
  }
1518
1619
  return {
@@ -1523,7 +1624,7 @@ async function detectProjectSnapshot(rootDir) {
1523
1624
  };
1524
1625
  }
1525
1626
  async function getCodeSamples(dir, count = 15) {
1526
- const rootDir = path5.resolve(dir);
1627
+ const rootDir = path6.resolve(dir);
1527
1628
  const samples = await sampleFiles(rootDir, count);
1528
1629
  const output = {
1529
1630
  note: "These are previews (first ~60 lines). Use get_file_content to read the full file if needed.",
@@ -1538,10 +1639,10 @@ async function getCodeSamples(dir, count = 15) {
1538
1639
  return JSON.stringify(output, null, 2);
1539
1640
  }
1540
1641
  async function getProjectStructure(dir) {
1541
- const rootDir = path5.resolve(dir);
1542
- const allFiles = await fg4("**/*", {
1642
+ const rootDir = path6.resolve(dir);
1643
+ const allFiles = await fg5("**/*", {
1543
1644
  cwd: rootDir,
1544
- ignore: IGNORE2,
1645
+ ignore: IGNORE3,
1545
1646
  onlyFiles: true
1546
1647
  });
1547
1648
  const dirInfo = /* @__PURE__ */ new Map();
@@ -1554,7 +1655,7 @@ async function getProjectStructure(dir) {
1554
1655
  }
1555
1656
  const info2 = dirInfo.get(dirPath);
1556
1657
  info2.fileCount++;
1557
- const ext = path5.extname(file).slice(1);
1658
+ const ext = path6.extname(file).slice(1);
1558
1659
  if (ext) {
1559
1660
  info2.extensions.set(ext, (info2.extensions.get(ext) ?? 0) + 1);
1560
1661
  }
@@ -1576,81 +1677,12 @@ async function getProjectStructure(dir) {
1576
1677
  return JSON.stringify(output, null, 2);
1577
1678
  }
1578
1679
  async function getTestMap(dir) {
1579
- const rootDir = path5.resolve(dir);
1580
- const testPatterns = [
1581
- "**/*.test.*",
1582
- "**/*.spec.*",
1583
- "**/*Test.kt",
1584
- "**/*Test.java",
1585
- "**/*Tests.kt",
1586
- "**/*Tests.java",
1587
- "**/test_*.py",
1588
- "**/*_test.py",
1589
- "**/*_test.go",
1590
- "**/*Tests.swift",
1591
- "**/*Test.swift",
1592
- "**/*_test.rs"
1593
- ];
1594
- const testFiles = await fg4(testPatterns, { cwd: rootDir, ignore: IGNORE2 });
1595
- const sourceFiles = await fg4(
1596
- "**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,dart}",
1597
- { cwd: rootDir, ignore: IGNORE2 }
1598
- );
1599
- const sourceByBaseName = /* @__PURE__ */ new Map();
1600
- for (const file of sourceFiles) {
1601
- if (testFiles.includes(file)) continue;
1602
- const baseName = path5.basename(file).replace(/\.[^.]+$/, "");
1603
- const existing = sourceByBaseName.get(baseName) ?? [];
1604
- existing.push(file);
1605
- sourceByBaseName.set(baseName, existing);
1606
- }
1607
- const pairs = [];
1608
- const unmatched = [];
1609
- for (const testFile of testFiles) {
1610
- const testBaseName = path5.basename(testFile).replace(/\.[^.]+$/, "");
1611
- const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
1612
- if (!sourceName) {
1613
- unmatched.push(testFile);
1614
- continue;
1615
- }
1616
- const candidates = sourceByBaseName.get(sourceName);
1617
- if (candidates && candidates.length > 0) {
1618
- const testDir = path5.dirname(testFile);
1619
- const bestMatch = candidates.reduce((best, candidate) => {
1620
- const candidateDir = path5.dirname(candidate);
1621
- const bestDir = path5.dirname(best);
1622
- const candidateOverlap = commonSegments(testDir, candidateDir);
1623
- const bestOverlap = commonSegments(testDir, bestDir);
1624
- return candidateOverlap > bestOverlap ? candidate : best;
1625
- });
1626
- pairs.push({
1627
- test: testFile,
1628
- source: bestMatch,
1629
- confidence: candidates.length === 1 ? "exact" : "best-guess"
1630
- });
1631
- } else {
1632
- unmatched.push(testFile);
1633
- }
1634
- }
1635
- const output = {
1636
- totalTestFiles: testFiles.length,
1637
- paired: pairs,
1638
- unmatched
1639
- };
1640
- return JSON.stringify(output, null, 2);
1641
- }
1642
- function commonSegments(pathA, pathB) {
1643
- const segsA = pathA.split("/");
1644
- const segsB = pathB.split("/");
1645
- let count = 0;
1646
- for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
1647
- if (segsA[i] === segsB[i]) count++;
1648
- else break;
1649
- }
1650
- return count;
1680
+ const { buildTestMap: buildTestMap2 } = await Promise.resolve().then(() => (init_test_map(), test_map_exports));
1681
+ const result = await buildTestMap2(dir);
1682
+ return JSON.stringify(result, null, 2);
1651
1683
  }
1652
1684
  async function getSnapshot(dir) {
1653
- const rootDir = path5.resolve(dir);
1685
+ const rootDir = path6.resolve(dir);
1654
1686
  const snapshot = await loadSnapshot(rootDir);
1655
1687
  if (!snapshot) {
1656
1688
  return JSON.stringify({
@@ -1666,7 +1698,11 @@ async function getSnapshot(dir) {
1666
1698
  const unique = feat.files.filter((f) => !seenFiles.has(f));
1667
1699
  if (unique.length === 0) continue;
1668
1700
  for (const f of unique) seenFiles.add(f);
1669
- compactFeatures[name] = unique;
1701
+ const entry = { files: unique };
1702
+ if (feat.tests && feat.tests.length > 0) {
1703
+ entry.tests = feat.tests;
1704
+ }
1705
+ compactFeatures[name] = entry;
1670
1706
  }
1671
1707
  const compactFlows = {};
1672
1708
  for (const [name, flow] of Object.entries(snapshot.flows)) {
@@ -1674,6 +1710,7 @@ async function getSnapshot(dir) {
1674
1710
  }
1675
1711
  const output = {
1676
1712
  exists: true,
1713
+ updatedAt: snapshot.updatedAt,
1677
1714
  features: compactFeatures,
1678
1715
  flows: compactFlows,
1679
1716
  stale: isStale
@@ -1684,7 +1721,7 @@ async function getSnapshot(dir) {
1684
1721
  return JSON.stringify(output);
1685
1722
  }
1686
1723
  async function fullAnalysis(dir) {
1687
- const rootDir = path5.resolve(dir);
1724
+ const rootDir = path6.resolve(dir);
1688
1725
  const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
1689
1726
  analyzeProject(dir),
1690
1727
  getProjectStructure(dir),
@@ -1710,7 +1747,7 @@ async function fullAnalysis(dir) {
1710
1747
  return JSON.stringify(output, null, 2);
1711
1748
  }
1712
1749
  async function saveSnapshotData(dir, features, flows) {
1713
- const rootDir = path5.resolve(dir);
1750
+ const rootDir = path6.resolve(dir);
1714
1751
  const gitHash = await getCurrentGitHash(rootDir);
1715
1752
  const now = (/* @__PURE__ */ new Date()).toISOString();
1716
1753
  const existing = await loadSnapshot(rootDir);
@@ -1743,11 +1780,11 @@ async function saveSnapshotData(dir, features, flows) {
1743
1780
  }
1744
1781
  async function getImpact(dir, files) {
1745
1782
  const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
1746
- const rootDir = path5.resolve(dir);
1783
+ const rootDir = path6.resolve(dir);
1747
1784
  const result = await analyzeImpact2(rootDir, files);
1748
1785
  return JSON.stringify(result, null, 2);
1749
1786
  }
1750
- var exec8, IGNORE2;
1787
+ var exec8, IGNORE3;
1751
1788
  var init_tools = __esm({
1752
1789
  "src/mcp/tools.ts"() {
1753
1790
  "use strict";
@@ -1756,7 +1793,7 @@ var init_tools = __esm({
1756
1793
  init_sampler();
1757
1794
  init_snapshot();
1758
1795
  exec8 = promisify8(execFile8);
1759
- IGNORE2 = [
1796
+ IGNORE3 = [
1760
1797
  "**/node_modules/**",
1761
1798
  "**/dist/**",
1762
1799
  "**/build/**",
@@ -1786,7 +1823,7 @@ function createMcpServer() {
1786
1823
  const server = new McpServer(
1787
1824
  {
1788
1825
  name: "mason",
1789
- version: "0.2.2"
1826
+ version: "0.3.0"
1790
1827
  },
1791
1828
  {
1792
1829
  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."
@@ -1907,7 +1944,7 @@ init_providers();
1907
1944
  init_tools();
1908
1945
  import { Command } from "commander";
1909
1946
  import fs7 from "fs/promises";
1910
- import path6 from "path";
1947
+ import path7 from "path";
1911
1948
  import ora from "ora";
1912
1949
  import chalk2 from "chalk";
1913
1950
 
@@ -1970,7 +2007,7 @@ function createCLI() {
1970
2007
  const program2 = new Command();
1971
2008
  program2.name("mason").description(
1972
2009
  "Context engineering CLI & MCP server \u2014 generates intelligent CLAUDE.md files"
1973
- ).version("0.1.0");
2010
+ ).version("0.3.0");
1974
2011
  program2.command("setup").description("Register Mason as an MCP server with Claude Code").option("--scope <scope>", "Config scope: user or project", "user").action(async (opts) => {
1975
2012
  const { execFile: execFile9 } = await import("child_process");
1976
2013
  const { promisify: promisify9 } = await import("util");
@@ -2050,7 +2087,7 @@ function createCLI() {
2050
2087
  );
2051
2088
  process.exit(1);
2052
2089
  }
2053
- const rootDir = path6.resolve(dir);
2090
+ const rootDir = path7.resolve(dir);
2054
2091
  const runConfig = opts.model ? { ...config, model: opts.model } : config;
2055
2092
  const spinner = ora("Analyzing codebase...").start();
2056
2093
  const analysisData = await fullAnalysis(rootDir);
@@ -2080,9 +2117,9 @@ ${analysisData}`
2080
2117
  error("LLM returned empty response.");
2081
2118
  process.exit(1);
2082
2119
  }
2083
- const claudeDir = path6.join(rootDir, ".claude");
2120
+ const claudeDir = path7.join(rootDir, ".claude");
2084
2121
  await fs7.mkdir(claudeDir, { recursive: true });
2085
- const outPath = path6.join(claudeDir, "CLAUDE.md");
2122
+ const outPath = path7.join(claudeDir, "CLAUDE.md");
2086
2123
  await fs7.writeFile(outPath, markdown, "utf-8");
2087
2124
  success(`Generated ${outPath}`);
2088
2125
  try {
@@ -2097,7 +2134,7 @@ ${analysisData}`
2097
2134
  );
2098
2135
  } catch {
2099
2136
  }
2100
- const hookPath = path6.join(rootDir, ".git", "hooks", "post-commit");
2137
+ const hookPath = path7.join(rootDir, ".git", "hooks", "post-commit");
2101
2138
  try {
2102
2139
  const hookContent = await fs7.readFile(hookPath, "utf-8");
2103
2140
  if (!hookContent.includes("mason snapshot-update")) {
@@ -2127,7 +2164,7 @@ ${analysisData}`
2127
2164
  createSnapshot: createSnapshot2,
2128
2165
  installHook: installHook2
2129
2166
  } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
2130
- const rootDir = path6.resolve(dir);
2167
+ const rootDir = path7.resolve(dir);
2131
2168
  if (opts.installHook) {
2132
2169
  try {
2133
2170
  await installHook2(rootDir);
@@ -2165,7 +2202,7 @@ ${analysisData}`
2165
2202
  });
2166
2203
  program2.command("snapshot-update").description("Incrementally update snapshot with recent changes").argument("[dir]", "Directory to update", ".").action(async (dir) => {
2167
2204
  const { updateSnapshot: updateSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
2168
- const rootDir = path6.resolve(dir);
2205
+ const rootDir = path7.resolve(dir);
2169
2206
  const config = await loadConfig();
2170
2207
  if (!config) return;
2171
2208
  try {
@@ -2179,7 +2216,7 @@ ${analysisData}`
2179
2216
  });
2180
2217
  program2.command("impact").description("Show files affected by changes to a given file").argument("<files...>", "File paths or names to analyze").option("-d, --dir <dir>", "Project directory", ".").action(async (files, opts) => {
2181
2218
  const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
2182
- const rootDir = path6.resolve(opts.dir);
2219
+ const rootDir = path7.resolve(opts.dir);
2183
2220
  const spinner = ora("Analyzing impact...").start();
2184
2221
  const result = await analyzeImpact2(rootDir, files);
2185
2222
  spinner.stop();
@@ -2216,7 +2253,7 @@ Impact analysis for: ${result.targetFiles.join(", ")}
2216
2253
  }
2217
2254
  });
2218
2255
  program2.command("analyze").description("Analyze the codebase and print findings").argument("[dir]", "Directory to analyze", ".").action(async (dir) => {
2219
- const rootDir = path6.resolve(dir);
2256
+ const rootDir = path7.resolve(dir);
2220
2257
  const spinner = ora("Analyzing codebase...").start();
2221
2258
  const context = await buildContext2(rootDir);
2222
2259
  const results = await runAll(context);